/* ===== MusicMood — logika frontendu ===== */ const video = document.getElementById("video"); const canvas = document.getElementById("canvas"); const flash = document.getElementById("flash"); const cameraMsg = document.getElementById("cameraMsg"); const cameraMsgText = document.getElementById("cameraMsgText"); const enableCamBtn = document.getElementById("enableCamBtn"); const cameraSelect = document.getElementById("cameraSelect"); const scanBtn = document.getElementById("scanBtn"); const appPassInput = document.getElementById("appPass"); const statusEl = document.getElementById("status"); const resultsEl = document.getElementById("results"); const tvScreen = document.getElementById("tvScreen"); const tvStatic = document.getElementById("tvStatic"); const tvLed = document.getElementById("tvLed"); let currentStream = null; const DNI = ["niedziela", "poniedziałek", "wtorek", "środa", "czwartek", "piątek", "sobota"]; /* ---------- kamery ---------- */ async function startStream(deviceId) { if (currentStream) currentStream.getTracks().forEach((t) => t.stop()); const constraints = { video: deviceId ? { deviceId: { exact: deviceId }, width: { ideal: 1280 }, height: { ideal: 960 } } : { width: { ideal: 1280 }, height: { ideal: 960 } }, audio: false, }; currentStream = await navigator.mediaDevices.getUserMedia(constraints); video.srcObject = currentStream; cameraMsg.classList.add("hidden"); scanBtn.disabled = false; } async function listCameras() { const devices = await navigator.mediaDevices.enumerateDevices(); const cams = devices.filter((d) => d.kind === "videoinput"); cameraSelect.innerHTML = ""; cams.forEach((cam, i) => { const opt = document.createElement("option"); opt.value = cam.deviceId; opt.textContent = cam.label || `Kamera ${i + 1}`; cameraSelect.appendChild(opt); }); return cams; } async function initCameras() { if (!navigator.mediaDevices?.getUserMedia) { cameraMsgText.textContent = "Twoja przeglądarka nie obsługuje dostępu do kamery."; return; } try { // najpierw poproś o dostęp — bez tego enumerateDevices nie zwraca nazw kamer await startStream(null); const cams = await listCameras(); if (cams.length === 0) { cameraMsgText.textContent = "Nie wykryto żadnej kamery w systemie."; enableCamBtn.hidden = true; cameraMsg.classList.remove("hidden"); return; } // zaznacz w select aktywną kamerę const activeId = currentStream.getVideoTracks()[0].getSettings().deviceId; if (activeId) cameraSelect.value = activeId; } catch (err) { // część przeglądarek blokuje kamerę bez gestu użytkownika — // pokaż przycisk, którego kliknięcie ponawia próbę cameraMsgText.textContent = `Kamera nie wystartowała automatycznie (${err.name}${err.message ? ": " + err.message : ""}). ` + "Kliknij poniżej i zezwól na dostęp."; enableCamBtn.hidden = false; cameraMsg.classList.remove("hidden"); } } enableCamBtn.addEventListener("click", () => { cameraMsgText.textContent = "Uruchamianie kamery…"; enableCamBtn.hidden = true; initCameras(); }); cameraSelect.addEventListener("change", async () => { try { await startStream(cameraSelect.value); } catch { cameraMsgText.textContent = "Nie udało się uruchomić wybranej kamery."; cameraMsg.classList.remove("hidden"); } }); /* ---------- geolokalizacja ---------- */ function getPosition() { return new Promise((resolve) => { if (!navigator.geolocation) return resolve(null); navigator.geolocation.getCurrentPosition( (pos) => resolve({ lat: pos.coords.latitude, lon: pos.coords.longitude }), () => resolve(null), { timeout: 6000, maximumAge: 600000 } ); }); } /* ---------- zdjęcie ---------- */ function captureFrame() { canvas.width = video.videoWidth; canvas.height = video.videoHeight; canvas.getContext("2d").drawImage(video, 0, 0); // dataURL -> czysty base64 return canvas.toDataURL("image/jpeg", 0.85).split(",")[1]; } function doFlash() { flash.classList.add("on"); setTimeout(() => flash.classList.remove("on"), 60); } /* ---------- retro logi na ekranie TV ---------- */ const tvLog = document.getElementById("tvLog"); const IDLE_LOGS = [ "MOODTRON BIOS v0.62 ........ OK", "strojenie anteny do fal nastroju…", "odkurzanie lamp kineskopu…", "nasłuch eteru: cisza, tylko szum kosmiczny", "kalibracja gałki nastroju… ćwierć obrotu w prawo", "czekam na sygnał… może herbatka?", "liczę owce w paśmie UHF… doliczyłem do 42", "prasowanie pasków testowych…", "ziewam w częstotliwości 50 Hz…", "sprawdzam, czy ktoś patrzy… chyba tak", "smarowanie kondensatorów olejem lnianym…", "trening kineskopu: wdech, wydech…", "polerowanie elektronów na zapas…", "nuda… nawet szum ma dziś wolne", "wciąż gotowy. wciśnij SCAN, nie wstydź się", ]; const SCAN_LOGS = [ "przechwytywanie fotonów twarzy…", "wysyłanie pikseli do mózgu elektronowego…", "GPT marszczy brwi nad Twoimi brwiami…", "mierzenie kącików ust suwmiarką…", "konsultacja z działem emocji…", "sprawdzanie ciśnienia (atmosferycznego, nie Twojego)…", "pytam chmury, jaka pogoda… dosłownie", "przeszukiwanie szafy z winylami…", "DJ MoodTron przegląda skrzynki z płytami…", "strzepywanie kurzu z igły gramofonu…", "negocjacje z YouTube w sprawie sygnału…", "dostrajanie tempa do Twojego pulsu…", "liczenie BPM na liczydle…", "przesłuchanie sekcji rytmicznej w toku…", "kalibrowanie nastroju do dnia tygodnia…", "muzy zwołane na naradę…", ]; let logTimer = null; let logPool = []; let logIdx = 0; function logLine() { const t = new Date().toLocaleTimeString("pl-PL"); const line = document.createElement("div"); line.textContent = `[${t}] ${logPool[logIdx % logPool.length]}`; logIdx++; tvLog.appendChild(line); while (tvLog.children.length > 9) tvLog.removeChild(tvLog.firstChild); } function startLogs(mode) { clearInterval(logTimer); logPool = [...(mode === "scan" ? SCAN_LOGS : IDLE_LOGS)] .sort(() => Math.random() - 0.5); logIdx = 0; logLine(); logTimer = setInterval(logLine, mode === "scan" ? 1100 : 2600); } function stopLogs() { clearInterval(logTimer); logTimer = null; } /* ---------- TV (YouTube IFrame API) ---------- */ let ytPlayer = null; let ytReady = false; let videoQueue = []; let queueIndex = 0; window.onYouTubeIframeAPIReady = () => { ytReady = true; }; function playNextInQueue() { queueIndex++; if (queueIndex < videoQueue.length) { setStatus(`Film niedostępny — próbuję następny wynik (${queueIndex + 1}/${videoQueue.length})…`, "busy"); ytPlayer.loadVideoById(videoQueue[queueIndex]); } else { setStatus("Żaden ze znalezionych filmów nie jest dostępny do osadzenia. Kliknij SCAN ponownie.", "error"); } } function playOnTV(videoIds) { videoQueue = videoIds; queueIndex = 0; stopLogs(); tvStatic.classList.add("off"); tvLed.classList.add("on"); if (ytPlayer) { ytPlayer.loadVideoById(videoIds[0]); return; } if (!ytReady || !window.YT) { setStatus("Player YouTube jeszcze się ładuje — spróbuj za chwilę.", "error"); return; } ytPlayer = new YT.Player("ytplayer", { width: "100%", height: "100%", videoId: videoIds[0], playerVars: { autoplay: 1, rel: 0 }, events: { onReady: (e) => e.target.playVideo(), onError: playNextInQueue, }, }); } /* ---------- wyniki ---------- */ function setText(id, value) { document.getElementById(id).textContent = value ?? "—"; } function showResults(data) { const o = data.osoba; setText("rPlec", o?.plec); setText("rWiek", o?.wiek); setText("rNastroj", o?.nastroj); setText("rEmocje", o?.emocje?.join(", ")); const k = data.kontekst || {}; setText("rCzas", `${k.dzien_tygodnia}, ${k.czas_lokalny}`); const p = k.pogoda; if (p && typeof p === "object") { const city = p.miejscowosc ? `${p.miejscowosc}: ` : ""; setText("rPogoda", `${city}${p.opis}, ${p.temperatura_c}°C, wiatr ${p.wiatr_kmh} km/h`); setText("rCisnienie", p.cisnienie_hpa ? `${Math.round(p.cisnienie_hpa)} hPa` : "—"); } else { setText("rPogoda", "brak danych"); setText("rCisnienie", "—"); } const r = data.rekomendacja || {}; setText("rStyl", r.styl); setText("rTempo", r.tempo); setText("rRytmika", r.rytmika); setText("rCharakter", r.charakter); setText("rUzasadnienie", r.uzasadnienie); setText("rQuery", data.youtube_query); resultsEl.hidden = false; } function setStatus(text, cls = "") { statusEl.textContent = text; statusEl.className = "status " + cls; } /* ---------- SCAN ---------- */ scanBtn.addEventListener("click", async () => { const password = appPassInput.value.trim(); if (!password) { setStatus("Wpisz najpierw hasło dostępu.", "error"); appPassInput.focus(); return; } if (!video.videoWidth) { setStatus("Kamera jeszcze się nie uruchomiła.", "error"); return; } scanBtn.disabled = true; doFlash(); const imageB64 = captureFrame(); // TV wraca na "szum" z logami analizy; ewentualna muzyka stop if (ytPlayer && ytPlayer.stopVideo) ytPlayer.stopVideo(); tvLed.classList.remove("on"); tvStatic.classList.remove("off"); startLogs("scan"); setStatus("Pobieranie lokalizacji i analiza zdjęcia (GPT-4o)…", "busy"); const pos = await getPosition(); const now = new Date(); const payload = { password: password, image_b64: imageB64, local_time: now.toLocaleString("pl-PL", { dateStyle: "short", timeStyle: "short" }), day_of_week: DNI[now.getDay()], timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, lat: pos?.lat ?? null, lon: pos?.lon ?? null, }; try { const resp = await fetch("/api/analyze", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const data = await resp.json(); if (!resp.ok) throw new Error(data.detail || "Błąd serwera"); showResults(data); if (data.video_ids && data.video_ids.length > 0) { playOnTV(data.video_ids); setStatus("Gotowe! Utwór gra na MoodTronie 📺 (jeśli nie wystartował sam — kliknij ▶ na ekranie TV)."); } else { setStatus("Analiza gotowa, ale nie udało się znaleźć utworu na YouTube — spróbuj ponownie.", "error"); startLogs("idle"); } } catch (err) { setStatus("Błąd: " + err.message, "error"); startLogs("idle"); } finally { scanBtn.disabled = false; } }); /* ---------- start ---------- */ initCameras(); startLogs("idle"); navigator.mediaDevices?.addEventListener?.("devicechange", listCameras);