mirror of
https://github.com/adelatour11/codebarr.git
synced 2025-12-29 19:19:55 -06:00
Refactor index.html for improved scanner functionality
Refactor HTML structure and scripts for barcode scanner functionality, integrating ZXing library for barcode detection and improving service worker registration.
This commit is contained in:
@@ -1,287 +1,287 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Lidarr Barcode Scanner</title>
|
||||
<link rel="icon" type="image/x-icon" href="static/icons/icon-192.png">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Lidarr Barcode Scanner</title>
|
||||
<link rel="icon" type="image/x-icon" href="static/icons/icon-192.png">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500;700&display=swap" rel="stylesheet">
|
||||
<script src="https://unpkg.com/@zxing/browser@latest"></script>
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500;700&display=swap" rel="stylesheet">
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/static/service-worker.js')
|
||||
.then(() => console.log("✅ Service worker registered"))
|
||||
.catch(err => console.error("Service worker error:", err));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/quagga@0.12.1/dist/quagga.min.js"></script>
|
||||
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/static/service-worker.js')
|
||||
.then(() => console.log("✅ Service worker registered"))
|
||||
.catch(err => console.error("Service worker error:", err));
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: inherit;
|
||||
}
|
||||
html, body {
|
||||
font-family: 'Roboto', 'Open Sans', sans-serif;
|
||||
font-weight: 100; /* Thin */
|
||||
}
|
||||
body {
|
||||
font-family: 'Roboto', 'Open Sans', sans-serif;
|
||||
background: #202020;
|
||||
color: #eee;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-weight: 100; /* Thin */
|
||||
|
||||
}
|
||||
h1 { color: #5c9d4e; margin-bottom: 10px; font-weight: 300; /* Thin */}
|
||||
#lidarr-logo { max-width: 128px; margin: 10px auto; display: block; }
|
||||
#status { margin: 10px 0; font-weight: 300; }
|
||||
#preview-container { position: relative; width: 100%; max-width: 500px; margin: 20px auto; border: 2px solid #5c9d4e; border-radius: 4px; overflow: hidden; background: #000; height: 300px; }
|
||||
#scanner-guides::before { content: ""; position: absolute; top: 20%; left: 10%; right: 10%; height: 60%; border: 2px dashed rgba(92,157,78,0.7); }
|
||||
.button-group { display: flex; gap: 10px; justify-content: center; margin: 15px 0; }
|
||||
.button-group button { flex: 1; padding: 10px; border: none; border-radius: 4px; background: #5c9d4e; color: #fff; font-weight: 300; cursor: pointer; }
|
||||
.button-group button:hover { background: #4b8440; }
|
||||
#toggle-container { display: flex; justify-content: center; gap: 10px; margin: 10px 0; }
|
||||
#toggle-container button { padding: 10px; border-radius: 4px; border: none; background: #888; color: #fff; cursor: pointer; flex: 1; font-weight: 300;}
|
||||
#barcode { padding: 8px; width: 200px; margin: 10px 0; border-radius: 4px; border: 1px solid #5c9d4e; }
|
||||
.progress-container { width: 80%; height: 20px; background: #333; border-radius: 10px; margin: 10px auto; overflow: hidden; }
|
||||
.progress-bar { height: 100%; width: 0%; background: #5c9d4e; transition: width 0.3s; }
|
||||
#debug-info { margin-top: 10px; color: #ccc; }
|
||||
#album-info { margin-top: 10px; font-weight: 300;}
|
||||
#add-album-btn { padding: 10px 20px; margin-top: 10px; border-radius: 4px; border: none; background: #5c9d4e; color: #fff; font-weight: 300; cursor: pointer; display: none; }
|
||||
#add-album-btn:hover { background: #4b8440; }
|
||||
</style>
|
||||
<style>
|
||||
* { font-family: inherit; }
|
||||
html, body { font-family: 'Roboto', 'Open Sans', sans-serif; font-weight: 100; }
|
||||
body { background: #202020; color: #eee; text-align: center; padding: 20px; }
|
||||
h1 { color: #5c9d4e; margin-bottom: 10px; font-weight: 300; }
|
||||
#lidarr-logo { max-width: 128px; margin: 10px auto; display: block; }
|
||||
#status { margin: 10px 0; font-weight: 300; }
|
||||
#preview-container { position: relative; width: 100%; max-width: 500px; margin: 20px auto; border: 2px solid #5c9d4e; border-radius: 4px; overflow: hidden; background: #000; height: 300px; }
|
||||
#scanner-guides::before { content: ""; position: absolute; top: 20%; left: 10%; right: 10%; height: 60%; border: 2px dashed rgba(92,157,78,0.7); }
|
||||
.button-group { display: flex; gap: 10px; justify-content: center; margin: 15px 0; }
|
||||
.button-group button { flex: 1; padding: 10px; border: none; border-radius: 4px; background: #5c9d4e; color: #fff; font-weight: 300; cursor: pointer; }
|
||||
.button-group button:hover { background: #4b8440; }
|
||||
#toggle-container { display: flex; justify-content: center; gap: 10px; margin: 10px 0; }
|
||||
#toggle-container button { padding: 10px; border-radius: 4px; border: none; background: #888; color: #fff; cursor: pointer; flex: 1; font-weight: 300;}
|
||||
#barcode { padding: 8px; width: 200px; margin: 10px 0; border-radius: 4px; border: 1px solid #5c9d4e; }
|
||||
.progress-container { width: 80%; height: 20px; background: #333; border-radius: 10px; margin: 10px auto; overflow: hidden; }
|
||||
.progress-bar { height: 100%; width: 0%; background: #5c9d4e; transition: width 0.3s; }
|
||||
#debug-info { margin-top: 10px; color: #ccc; }
|
||||
#album-info { margin-top: 10px; font-weight: 300;}
|
||||
#add-album-btn { padding: 10px 20px; margin-top: 10px; border-radius: 4px; border: none; background: #5c9d4e; color: #fff; font-weight: 300; cursor: pointer; display: none; }
|
||||
#add-album-btn:hover { background: #4b8440; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<img id="lidarr-logo" src="static/icons/icon-192.png" alt="Lidarr Logo">
|
||||
<h1>Lidarr Barcode Scanner</h1>
|
||||
<div id="status">Scanner stopped.</div>
|
||||
|
||||
<img id="lidarr-logo" src="static/icons/icon-192.png" alt="Lidarr Logo">
|
||||
<h1>Lidarr Barcode Scanner</h1>
|
||||
<div id="status"> Scanner stopped.</div>
|
||||
|
||||
<div id="preview-container">
|
||||
<div id="preview-container">
|
||||
<div id="preview"></div>
|
||||
<div id="scanner-guides"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<div class="button-group">
|
||||
<button id="start-scanner">Start Scanner</button>
|
||||
<button id="stop-scanner">Stop Scanner</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toggle-container">
|
||||
<button id="reset-scanner"> Reset</button>
|
||||
</div>
|
||||
<div id="toggle-container">
|
||||
<button id="reset-scanner">Reset</button>
|
||||
</div>
|
||||
|
||||
<input type="text" id="barcode" placeholder="EAN-13 or UPC barcode or enter manually">
|
||||
<input type="text" id="barcode" placeholder="EAN-13 or UPC barcode or enter manually">
|
||||
<div id="album-info"></div>
|
||||
<button id="add-album-btn">Add Album to Lidarr</button>
|
||||
|
||||
<div id="album-info"></div>
|
||||
<button id="add-album-btn">Add Album to Lidarr</button>
|
||||
<div class="progress-container">
|
||||
<div id="progress-bar" class="progress-bar"></div>
|
||||
</div>
|
||||
|
||||
<div class="progress-container"><div id="progress-bar" class="progress-bar"></div></div>
|
||||
<div id="debug-info"></div>
|
||||
<div id="debug-info"></div>
|
||||
|
||||
<div id="app-control" style="margin-top: 20px;">
|
||||
<button id="stop-app-btn" style="padding: 10px 20px; border-radius: 4px; border: none; background: #c0392b; color: #fff; font-weight: 300; cursor: pointer;">Stop App</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let scannerRunning = false;
|
||||
let codeReader = null;
|
||||
let currentBarcode = null;
|
||||
let detectionHandled = false; // Flag to track if detection is already handled
|
||||
|
||||
|
||||
<div id="app-control" style="margin-top: 20px;">
|
||||
<button id="stop-app-btn" style="
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
background: #c0392b;
|
||||
color: #fff;
|
||||
font-weight: 300;
|
||||
cursor: pointer;
|
||||
"> Stop App</button>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
let scannerRunning = false;
|
||||
let videoTrack = null;
|
||||
let flashlightOn = false;
|
||||
let currentBarcode = null;
|
||||
// --- UTILS ---
|
||||
function updateStatus(msg) {
|
||||
document.getElementById('status').innerText = msg;
|
||||
}
|
||||
|
||||
// --- SCANNER LOGIC ---
|
||||
function updateStatus(msg){ document.getElementById('status').innerText = msg; }
|
||||
function updateProgress(percent){ document.getElementById('progress-bar').style.width = percent + "%"; }
|
||||
function updateProgress(percent) {
|
||||
document.getElementById('progress-bar').style.width = percent + "%";
|
||||
}
|
||||
|
||||
function startScanner(){
|
||||
if(scannerRunning) return;
|
||||
// --- SCANNER ---
|
||||
|
||||
async function startScanner() {
|
||||
if (scannerRunning) return;
|
||||
scannerRunning = true;
|
||||
detectionHandled = false; // Reset the flag
|
||||
updateStatus("Initializing scanner...");
|
||||
|
||||
Quagga.init({
|
||||
inputStream: { name: "Live", type: "LiveStream", target: document.getElementById('preview'), constraints: { facingMode: "environment", width: { ideal: 640 }, height: { ideal: 480 }, focusMode: "continuous"} },
|
||||
locator: {
|
||||
halfSample: false, // process full image (not downsampled)
|
||||
patchSize: "large", // "x-large" for longer barcodes
|
||||
debug: {
|
||||
showCanvas: false,
|
||||
showPatches: false,
|
||||
showFoundPatches: false,
|
||||
showSkeleton: false,
|
||||
showLabels: false,
|
||||
showPatchLabels: false,
|
||||
showRemainingPatchLabels: false,
|
||||
boxFromPatches: {
|
||||
showTransformed: true,
|
||||
showTransformedBox: true,
|
||||
showBB: true
|
||||
}
|
||||
}
|
||||
},
|
||||
numOfWorkers: navigator.hardwareConcurrency || 4,
|
||||
frequency: 5,
|
||||
decoder: { readers: ["ean_reader","upc_reader"], multiple: false },
|
||||
locate: true, singleChannel: false
|
||||
}, function(err){
|
||||
if(err){ console.error(err); return; }
|
||||
Quagga.start();
|
||||
codeReader = new ZXingBrowser.BrowserMultiFormatReader();
|
||||
const videoElement = document.createElement("video");
|
||||
videoElement.setAttribute("playsinline", true);
|
||||
videoElement.style.width = "100%";
|
||||
videoElement.style.height = "100%";
|
||||
const preview = document.getElementById("preview");
|
||||
preview.innerHTML = "";
|
||||
preview.appendChild(videoElement);
|
||||
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: "environment", width: { ideal: 640 }, height: { ideal: 480 } },
|
||||
});
|
||||
videoElement.srcObject = stream;
|
||||
|
||||
await codeReader.decodeFromVideoDevice(
|
||||
null,
|
||||
videoElement,
|
||||
async (result, err) => {
|
||||
if (result && !detectionHandled) {
|
||||
detectionHandled = true; // Mark detection as handled
|
||||
const code = result.getText();
|
||||
currentBarcode = code;
|
||||
document.getElementById("barcode").value = code;
|
||||
updateStatus(`📀 Barcode detected: ${code}`);
|
||||
await fetchAlbumInfo(code);
|
||||
}
|
||||
}
|
||||
);
|
||||
updateStatus("Scanner ready. Align barcode inside the guides.");
|
||||
const videoEl = document.querySelector('#preview video');
|
||||
if(videoEl && videoEl.srcObject) videoTrack = videoEl.srcObject.getVideoTracks()[0];
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
updateStatus("❌ Camera error");
|
||||
scannerRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
Quagga.onDetected(result=>{
|
||||
const code = result.codeResult.code;
|
||||
currentBarcode = code;
|
||||
document.getElementById('barcode').value = code;
|
||||
updateStatus(` CD Barcode detected: ${code}`);
|
||||
stopScanner();
|
||||
fetchAlbumInfo(code);
|
||||
});
|
||||
}
|
||||
|
||||
function stopScanner(){
|
||||
if(!scannerRunning) return;
|
||||
Quagga.stop();
|
||||
if(videoTrack){ videoTrack.stop(); videoTrack=null; }
|
||||
scannerRunning=false;
|
||||
flashlightOn=false;
|
||||
function stopScanner() {
|
||||
if (!scannerRunning) return;
|
||||
const videoElement = document.querySelector("#preview video");
|
||||
if (videoElement && videoElement.srcObject) {
|
||||
const stream = videoElement.srcObject;
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
if (videoElement) {
|
||||
videoElement.pause();
|
||||
videoElement.srcObject = null;
|
||||
videoElement.remove();
|
||||
}
|
||||
scannerRunning = false;
|
||||
updateStatus("Scanner stopped.");
|
||||
}
|
||||
}
|
||||
|
||||
function resetScanner(){
|
||||
|
||||
|
||||
|
||||
|
||||
function resetScanner() {
|
||||
stopScanner();
|
||||
detectionHandled = false; // Reset the flag
|
||||
currentBarcode = null;
|
||||
document.getElementById('barcode').value = "";
|
||||
document.getElementById('album-info').innerHTML = "";
|
||||
document.getElementById('add-album-btn').style.display = "none";
|
||||
document.getElementById("barcode").value = "";
|
||||
document.getElementById("album-info").innerHTML = "";
|
||||
document.getElementById("add-album-btn").style.display = "none";
|
||||
updateProgress(0);
|
||||
updateStatus("Scanner reset.");
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('start-scanner').addEventListener('click', startScanner);
|
||||
document.getElementById('stop-scanner').addEventListener('click', stopScanner);
|
||||
document.getElementById('reset-scanner').addEventListener('click', resetScanner);
|
||||
|
||||
// --- FETCH ALBUM INFO FROM MUSICBRAINZ ---
|
||||
function fetchAlbumInfo(barcode){
|
||||
const infoDiv = document.getElementById('album-info');
|
||||
infoDiv.innerHTML = " Fetching album info...";
|
||||
|
||||
return fetch(`https://musicbrainz.org/ws/2/release/?query=barcode:${barcode}&fmt=json`)
|
||||
.then(resp => resp.json())
|
||||
.then(data => {
|
||||
if (!data.releases || data.releases.length === 0) {
|
||||
infoDiv.innerHTML = "❌ No album found for this barcode";
|
||||
document.getElementById('add-album-btn').style.display = 'none';
|
||||
return false; // explicitly return false
|
||||
}
|
||||
const release = data.releases[0];
|
||||
const artist = release['artist-credit'][0].name;
|
||||
const album = release.title;
|
||||
infoDiv.innerHTML = `<strong>${album}</strong> by <strong>${artist}</strong>`;
|
||||
document.getElementById('add-album-btn').style.display = 'inline-block';
|
||||
currentBarcode = barcode; // set current barcode here
|
||||
return true;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
infoDiv.innerHTML = "❌ Error fetching album info";
|
||||
document.getElementById('add-album-btn').style.display = 'none';
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// --- MANUAL ENTRY HANDLING ---
|
||||
const barcodeInput = document.getElementById('barcode');
|
||||
barcodeInput.addEventListener('keydown', async (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
document.getElementById('start-scanner').addEventListener('click', startScanner);
|
||||
document.getElementById('stop-scanner').addEventListener('click', stopScanner);
|
||||
document.getElementById('reset-scanner').addEventListener('click', resetScanner);
|
||||
|
||||
// --- FETCH ALBUM INFO ---
|
||||
async function fetchAlbumInfo(barcode) {
|
||||
const infoDiv = document.getElementById('album-info');
|
||||
infoDiv.innerHTML = "Fetching album info...";
|
||||
try {
|
||||
const resp = await fetch(`https://musicbrainz.org/ws/2/release/?query=barcode:${barcode}&fmt=json`);
|
||||
const data = await resp.json();
|
||||
if (!data.releases || data.releases.length === 0) {
|
||||
infoDiv.innerHTML = "❌ No album found for this barcode";
|
||||
document.getElementById('add-album-btn').style.display = 'none';
|
||||
return false;
|
||||
}
|
||||
const release = data.releases[0];
|
||||
const artist = release['artist-credit'][0].name;
|
||||
const album = release.title;
|
||||
infoDiv.innerHTML = `<strong>${album}</strong> by <strong>${artist}</strong>`;
|
||||
document.getElementById('add-album-btn').style.display = 'inline-block';
|
||||
currentBarcode = barcode;
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
infoDiv.innerHTML = "❌ Error fetching album info";
|
||||
document.getElementById('add-album-btn').style.display = 'none';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- MANUAL ENTRY ---
|
||||
document.getElementById('barcode').addEventListener('keydown', async (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
const code = event.target.value.trim();
|
||||
if (!code) return;
|
||||
|
||||
barcodeInput.disabled = true;
|
||||
event.target.disabled = true;
|
||||
updateStatus(`⏳ Searching for ${code}...`);
|
||||
const found = await fetchAlbumInfo(code);
|
||||
if (found) updateStatus(`Manual entry: ${code}`);
|
||||
else updateStatus(`❌ No album found for ${code}`);
|
||||
event.target.disabled = false;
|
||||
event.target.focus();
|
||||
}
|
||||
});
|
||||
|
||||
const found = await fetchAlbumInfo(code); // wait for result
|
||||
if (found) {
|
||||
updateStatus(` Manual entry: ${code}`);
|
||||
} else {
|
||||
updateStatus(`❌ No album found for ${code}`);
|
||||
}
|
||||
// --- ADD ALBUM TO LIDARR ---
|
||||
document.getElementById('add-album-btn').addEventListener('click', async () => {
|
||||
if (!currentBarcode) return;
|
||||
const addBtn = document.getElementById('add-album-btn');
|
||||
addBtn.disabled = true;
|
||||
updateStatus("Starting album add process...");
|
||||
updateProgress(0);
|
||||
|
||||
barcodeInput.disabled = false;
|
||||
barcodeInput.focus();
|
||||
}
|
||||
});
|
||||
try {
|
||||
const response = await fetch("/submit", {
|
||||
method: "POST",
|
||||
headers: {'Content-Type':'application/x-www-form-urlencoded'},
|
||||
body: `barcode=${currentBarcode}`
|
||||
});
|
||||
|
||||
|
||||
// --- ADD ALBUM TO LIDARR ---
|
||||
document.getElementById('add-album-btn').addEventListener('click', ()=>{
|
||||
if(!currentBarcode) return;
|
||||
document.getElementById('add-album-btn').disabled = true;
|
||||
updateStatus("Starting album add process...");
|
||||
updateProgress(0);
|
||||
fetch("/submit", { method: "POST", headers: {'Content-Type':'application/x-www-form-urlencoded'}, body: `barcode=${currentBarcode}`})
|
||||
.then(response=>{
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
return reader.read().then(function process({done, value}){
|
||||
if(done) return;
|
||||
buffer += decoder.decode(value, {stream:true});
|
||||
const lines = buffer.split("\n\n");
|
||||
buffer = lines.pop();
|
||||
for(const line of lines){
|
||||
if(line.trim()){
|
||||
try{
|
||||
const data = JSON.parse(line);
|
||||
updateStatus(data.status);
|
||||
updateProgress(data.progress);
|
||||
}catch(e){ console.error(e); }
|
||||
}
|
||||
|
||||
await reader.read().then(function process({done, value}) {
|
||||
if (done) return;
|
||||
buffer += decoder.decode(value, {stream:true});
|
||||
const lines = buffer.split("\n\n");
|
||||
buffer = lines.pop();
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
try {
|
||||
const data = JSON.parse(line);
|
||||
updateStatus(data.status);
|
||||
updateProgress(data.progress);
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
return reader.read().then(process);
|
||||
}
|
||||
return reader.read().then(process);
|
||||
});
|
||||
}).catch(err=>{
|
||||
updateStatus("❌ Error adding album");
|
||||
|
||||
updateStatus("✅ Album processed");
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
updateStatus("❌ Error adding album");
|
||||
} finally {
|
||||
resetScanner();
|
||||
addBtn.disabled = false;
|
||||
startScanner();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
document.getElementById("stop-app-btn").addEventListener("click", ()=>{
|
||||
if(confirm("Are you sure you want to stop Codebarr?")){
|
||||
updateStatus(" Stopping Flask server...");
|
||||
// --- STOP APP ---
|
||||
document.getElementById("stop-app-btn").addEventListener("click", () => {
|
||||
if(confirm("Are you sure you want to stop Codebarr?")) {
|
||||
updateStatus("Stopping Flask server...");
|
||||
fetch("/shutdown", { method: "POST" })
|
||||
.then(resp => resp.text())
|
||||
.then(txt => {
|
||||
.then(resp => resp.text())
|
||||
.then(txt => {
|
||||
updateStatus(txt);
|
||||
updateProgress(100);
|
||||
})
|
||||
.catch(err => {
|
||||
})
|
||||
.catch(err => {
|
||||
updateStatus("❌ Error stopping app");
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user