// === Daily Satsang Frontend (enhanced) === let allSatsangs = []; let player = null; let currentVideoId = null; let userEmail = null; let saveInterval = null; let initialLoaderShown = false; // ---- Helpers ---- const API = "https://bhagvat-mongodb.hf.space"; const timeKey = (vid) => `satsang_time_${vid}`; const extractId = (y) => (y || "").split("/").pop(); // Debounce utility function debounce(fn, delay=400){ let t; return (...a)=>{ clearTimeout(t); t=setTimeout(()=>fn(...a), delay); }; } // Prefer male-ish Hindi/Indian voice when possible function pickVoice() { const voices = speechSynthesis.getVoices(); // Strong filters first const byPref = [ v => /sa-IN|hi-IN/i.test(v.lang) && /(male|deep|bass)/i.test(v.name), v => /sa-IN|hi-IN/i.test(v.lang) && /(google)/i.test(v.name) && /(male|india)/i.test(v.name), v => /sa-IN|hi-IN/i.test(v.lang), v => /en-IN/i.test(v.lang) && /(male|india)/i.test(v.name), v => /en-IN/i.test(v.lang), ]; for (const pick of byPref) { const found = voices.find(pick); if (found) return found; } return voices[0]; // fallback } // ---- Init ---- window.onload = async () => { const token = localStorage.getItem('authToken'); if (token) { try { userEmail = jwt_decode(token)?.email; } catch {} } await fetchAllSatsangs(); await fetchUserSaved(); // fills favorites/playlists & counts // Show loader while we try to restore last watched const il = document.getElementById('initialLoader'); il.style.display = 'flex'; initialLoaderShown = true; await restoreLastOrKeep(); // If nothing was loaded (user has no last video), hide the loader if (initialLoaderShown) il.style.display = 'none'; }; // Fetch satsangs (all) async function fetchAllSatsangs(){ try{ const res = await fetch(`${API}/satsangs?type=all`); const data = await res.json(); allSatsangs = data.satsangs || []; }catch(e){ console.error("fetchAllSatsangs", e); } } // Restore last watched from backend (if user), else keep last from local lastPlayedVideoId async function restoreLastOrKeep(){ let lastId = localStorage.getItem("lastPlayedVideoId"); if (userEmail){ try{ const r = await fetch(`${API}/get_user_saved?email=${encodeURIComponent(userEmail)}`); const saved = await r.json(); if (saved?.last_video_id) lastId = saved.last_video_id; }catch(e){ console.warn("get_user_saved last_video_id failed", e); } } if (lastId){ const match = allSatsangs.find(s => (extractId(s.youtube) === lastId) || (s.youtube === lastId)); if (match){ await showSatsang(match, {autoPlay:true}); return; } } // If no match, do nothing (user can hit Play Random) } function showRandomLoader(){ const el = document.getElementById('randomLoader'); if (el) el.style.display = 'block'; } function hideRandomLoader(){ const el = document.getElementById('randomLoader'); if (el) el.style.display = 'none'; } // Load random with filters + exclude previously played local (played list deprecated in favor of DB history) async function loadNextSatsang(){ showRandomLoader(); try { const type = document.getElementById('satsangType').value; const tagSearch = document.getElementById('tagSearch').value.trim().toLowerCase(); let filtered = allSatsangs.filter(s=>{ if (!s?.youtube) return false; const matchesType = (type==='all') || (s.type===type); const matchesTag = !tagSearch || (Array.isArray(s.tags) && s.tags.some(t=>t.toLowerCase().includes(tagSearch))); return matchesType && matchesTag; }); if (!filtered.length){ alert("No satsang videos found to play."); return; } const pick = filtered[Math.floor(Math.random()*filtered.length)]; await showSatsang(pick, {autoPlay:true}); } finally { // if player starts, we'll also hide in onPlayerStateChange setTimeout(()=> hideRandomLoader(), 1200); // safety hide } } // --- Advanced Search: debounce + backend scoring; loads BEST match automatically const doSearch = debounce(async ()=>{ const q = document.getElementById('tagSearch').value.trim(); if (!q) return; const type = document.getElementById('satsangType').value; try{ const res = await fetch(`${API}/satsangs/search?q=${encodeURIComponent(q)}&type=${encodeURIComponent(type)}`); const data = await res.json(); const best = data.results?.[0]; if (best) await showSatsang(best, {autoPlay:true}); else alert("No matching satsangs found."); }catch(e){ console.error("search failed", e); } }, 500); document.addEventListener('input', (e)=>{ if (e.target && e.target.id === 'tagSearch') doSearch(); }); // ---- Player + state/time ---- function whenYTReady(cb){ if (window.YT && YT.Player) return cb(); const prev = window.onYouTubeIframeAPIReady; window.onYouTubeIframeAPIReady = function(){ prev && prev(); cb(); }; } async function showSatsang(data, opts={}){ document.getElementById("videoTitle").textContent = data.title || "Untitled"; document.getElementById("videoDescription").textContent = data.description || ""; document.getElementById("transcriptContent").textContent = Array.isArray(data.transcript) ? data.transcript.map(t=>t.text).join(" ") : (data.transcript || "No transcript available."); const videoId = extractId(data.youtube); currentVideoId = videoId; localStorage.setItem("lastPlayedVideoId", videoId); // tell backend "this is my last watched" + add to history_ids (no time) if (userEmail){ fetch(`${API}/user/set_last_watched`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({email:userEmail, video_id:videoId}) }).catch(()=>{}); } const startSeconds = Math.max(0, (parseFloat(localStorage.getItem(timeKey(videoId))) || 0) - 5); whenYTReady(()=>{ const container = document.getElementById('player'); if (!container){ console.error("Missing #player"); return; } const params = { height:'315', width:'100%', videoId, playerVars:{autoplay:1, start:startSeconds, rel:0, controls:1}, events:{ onStateChange:onPlayerStateChange } }; if (!player) player = new YT.Player('player', params); else { try { player.loadVideoById({ videoId, startSeconds }); } catch { player = new YT.Player('player', params); } } }); } function onPlayerStateChange(e){ if (!currentVideoId) return; if (e.data === YT.PlayerState.PLAYING){ hideRandomLoader(); const il = document.getElementById('initialLoader'); if (initialLoaderShown && il) { il.style.display = 'none'; initialLoaderShown = false; } clearInterval(saveInterval); saveInterval = setInterval(()=>{ const t = Math.floor(player?.getCurrentTime?.() || 0); if (t>0){ localStorage.setItem(timeKey(currentVideoId), t); // Optional: still ping backend to record history (id only) if (userEmail){ fetch(`${API}/update_progress`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({email:userEmail, video_id: currentVideoId}) }).catch(()=>{}); } } }, 8000); } if (e.data === YT.PlayerState.PAUSED || e.data === YT.PlayerState.ENDED){ clearInterval(saveInterval); saveInterval=null; const t = Math.floor(player?.getCurrentTime?.() || 0); if (t>0) localStorage.setItem(timeKey(currentVideoId), t); } } function cancelInitialAndPlayRandom(){ const il = document.getElementById('initialLoader'); if (il) il.style.display = 'none'; initialLoaderShown = false; loadNextSatsang(); } // ---- Favorites & Playlists (instant UI, titles not ids) ---- async function fetchUserSaved(){ if (!userEmail) return; const res = await fetch(`${API}/get_user_saved?email=${encodeURIComponent(userEmail)}`); const data = await res.json(); const div = document.getElementById("savedContent"); div.innerHTML = ""; const idToTitle = {}; (allSatsangs || []).forEach(s => { idToTitle[extractId(s.youtube)] = s.title || extractId(s.youtube); }); if (Array.isArray(data.favorites) && data.favorites.length){ const html = data.favorites.map(v=>{ const vid = extractId(v); const title = idToTitle[vid] || vid; return `
  • ${title}
  • `; }).join(""); div.innerHTML += `

    Favorites

    `; } if (data.playlists){ for (const [name, videos] of Object.entries(data.playlists)){ const html = (videos||[]).map(v=>{ const vid = extractId(v); const title = idToTitle[vid] || vid; return `
  • ${title}
  • `; }).join(""); div.innerHTML += `
    ${name}
    `; } } // Counts: short & long if (Array.isArray(data.history_ids)){ let short = 0, long = 0; const typeMap = {}; allSatsangs.forEach(s => typeMap[extractId(s.youtube)] = s.type); data.history_ids.forEach(id=>{ const t = typeMap[extractId(id)]; if (t === 'short') short++; if (t === 'long') long++; }); document.getElementById('shortCount').textContent = short; document.getElementById('longCount').textContent = long; } } function viewSaved(){ document.querySelector('.saved-block')?.scrollIntoView({behavior:'smooth', block:'start'}); } async function markFavorite(){ if (!currentVideoId || !userEmail){ alert("Login required to mark favorite."); return; } await fetch(`${API}/mark_favorite`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ email:userEmail, video_id: currentVideoId }) }); // Refresh list instantly await fetchUserSaved(); alert("Added to Favorites"); } async function addToPlaylist(){ const playlist = document.getElementById('playlistName').value.trim(); if (!playlist || !currentVideoId || !userEmail){ alert("Login and playlist name required."); return; } await fetch(`${API}/add_to_playlist`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ email:userEmail, playlist_name: playlist, video_id: currentVideoId }) }); await fetchUserSaved(); alert(`Added to "${playlist}" playlist.`); } async function removeFromPlaylist(){ const playlist = document.getElementById('playlistName').value.trim(); if (!playlist || !currentVideoId || !userEmail){ alert("Login and playlist name required."); return; } await fetch(`${API}/remove_from_playlist`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ email:userEmail, playlist_name: playlist, video_id: currentVideoId }) }); await fetchUserSaved(); alert(`Removed from "${playlist}".`); } async function renamePlaylistPrompt(){ if (!userEmail){ alert("Login required."); return; } const oldName = document.getElementById('playlistName').value.trim(); if (!oldName){ alert("Type the current playlist name first."); return; } const newName = prompt("Enter new playlist name:", oldName); if (!newName || newName.trim() === oldName) return; const res = await fetch(`${API}/rename_playlist`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ email:userEmail, old_name: oldName, new_name: newName.trim() }) }); const data = await res.json(); if (res.ok) { document.getElementById('playlistName').value = newName.trim(); await fetchUserSaved(); alert(`Playlist renamed to "${newName.trim()}"`); } else { alert(data.error || "Rename failed"); } } function loadFromId(videoId){ const match = allSatsangs.find(s => extractId(s.youtube) === videoId || s.youtube === videoId); if (match) showSatsang(match, {autoPlay:true}); } // ---- Lock / Unlock Overlay ---- function toggleLockScreen(){ const overlay = document.createElement("div"); overlay.id = "customLockOverlay"; Object.assign(overlay.style, { position:"fixed", top:"0", left:"0", width:"100vw", height:"100vh", background:"transparent", zIndex:"9999", display:"flex", justifyContent:"flex-end", alignItems:"flex-start", pointerEvents:"auto" }); const btn = document.createElement("button"); btn.innerHTML = "🔓"; btn.title = "Unlock Screen"; Object.assign(btn.style, { margin:"10px", padding:"10px", fontSize:"20px", backgroundColor:"white", borderRadius:"8px", boxShadow:"0 0 10px rgba(0,0,0,0.2)" }); btn.onclick = ()=>document.body.removeChild(overlay); overlay.appendChild(btn); document.body.appendChild(overlay); } function unlockScreen(e){ e?.stopPropagation?.(); document.getElementById('lockOverlay').style.display='none'; } /************ TTS: Respect user language, voice, rate, pitch ************/ /** Try to read TTS prefs from your existing popup/menu/state. * We support several sources so we don't break your UI: * - window.ttsPrefs = {lang, voiceName, rate, pitch, engine} * - localStorage keys: tts.lang, tts.voiceName, tts.rate, tts.pitch, tts.engine * - DOM (OPTIONAL): #ttsLang, #voiceSelect, #ttsRate, #ttsPitch, #ttsEngine * engine: 'browser' (default) or 'server' (optional if you also wired server TTS) */ function getTTSPrefs() { const pick = (domId, lsKey, fallback) => { const domEl = document.getElementById(domId); if (domEl && domEl.value) return domEl.value; const ls = localStorage.getItem(lsKey); if (ls !== null && ls !== undefined && ls !== '') return ls; return fallback; }; const fromWin = (window.ttsPrefs && typeof window.ttsPrefs === 'object') ? window.ttsPrefs : {}; const prefs = { lang: fromWin.lang || pick('ttsLang', 'tts.lang', 'hi-IN'), // default Hindi voiceName: fromWin.voiceName || pick('voiceSelect', 'tts.voiceName', ''), // exact voice name if saved rate: parseFloat(fromWin.rate ?? pick('ttsRate', 'tts.rate', '0.85')) || 0.85, pitch: parseFloat(fromWin.pitch ?? pick('ttsPitch', 'tts.pitch', '0.8')) || 0.8, engine: (fromWin.engine || pick('ttsEngine', 'tts.engine', 'browser')).toLowerCase() }; return prefs; } /** Promise that resolves when voices are loaded (Chrome quirk). */ function loadVoicesOnce() { return new Promise(resolve => { const have = speechSynthesis.getVoices(); if (have && have.length) return resolve(have); const handler = () => { const vs = speechSynthesis.getVoices(); if (vs && vs.length) { speechSynthesis.onvoiceschanged = null; resolve(vs); } }; speechSynthesis.onvoiceschanged = handler; // Edge case: poke the engine to trigger load setTimeout(() => handler(), 50); }); } /** Find a voice by exact name if provided; else pick best match for lang; else fallback. */ function resolveVoice(voices, { lang, voiceName }) { // exact voice name match first if (voiceName) { const byName = voices.find(v => v.name === voiceName); if (byName) return byName; } // next: best match inside selected language const sameLang = voices.filter(v => (v.lang || '').toLowerCase() === (lang || '').toLowerCase()); if (sameLang.length) { // try to prefer deeper/male-ish names if exist const preferred = sameLang.find(v => /(male|deep|bass|baritone)/i.test(v.name)) || sameLang[0]; return preferred; } // next: language prefix match (e.g., hi vs hi-IN) const langPrefix = (lang || '').split('-')[0]; if (langPrefix) { const pref = voices.filter(v => (v.lang || '').toLowerCase().startsWith(langPrefix.toLowerCase())); if (pref.length) return pref[0]; } // final fallback return voices[0]; } /** Split transcript into natural chunks: danda, punctuation, comma, pipe. */ function buildTranscriptChunks(text) { const parts = text.split(/[।॥.!?\u0964\u0965,|]\s*/).filter(Boolean); const chunks = []; let buf = ''; for (const p of parts) { const next = buf ? (buf + ' ' + p) : p; if (next.length > 200) { // keep chunks ~180-220 chars if (buf) chunks.push(buf.trim()); buf = p; } else { buf = next; } } if (buf) chunks.push(buf.trim()); return chunks; } /** Speak an array of chunks with the browser engine. */ async function speakChunksBrowser(chunks, prefs) { const voices = await loadVoicesOnce(); const v = resolveVoice(voices, prefs); // clear any residual queue try { speechSynthesis.cancel(); } catch {} ttsPaused = false; ttsUtterances = chunks.map(txt => { const u = new SpeechSynthesisUtterance(txt); u.voice = v; u.lang = v?.lang || prefs.lang || 'hi-IN'; u.rate = prefs.rate; // slower u.pitch = prefs.pitch; // deeper u.volume = 1.0; return u; }); await new Promise(resolve => { const speakNext = () => { if (!ttsUtterances.length) return resolve(); const u = ttsUtterances.shift(); u.onend = () => { if (!ttsPaused) setTimeout(speakNext, 150); }; u.onerror = () => { if (!ttsPaused) setTimeout(speakNext, 150); }; try { speechSynthesis.speak(u); } catch { setTimeout(speakNext, 150); } }; speakNext(); }); } /** OPTIONAL: server TTS if you support it (engine === 'server'). * Keep this if you already wired a server TTS route; otherwise ignore. * Expects POST /tts_hi_male (or your multi-lang route) {text, lang, rate, pitch, voiceName?} */ async function speakChunksServer(chunks, prefs) { // Map browser slider to SSML style values roughly const ssmlRate = prefs.rate <= 0.9 ? '-15%' : '-5%'; const ssmlPitch = prefs.pitch <= 0.85 ? '-4st' : '-2st'; for (const chunk of chunks) { const res = await fetch(`${API.replace('/satsangs','')}/tts_hi_male`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ text: chunk, lang: prefs.lang, // if your backend supports switching voices per lang rate: ssmlRate, pitch: ssmlPitch, voiceName: prefs.voiceName || '' // optional: exact server voice }) }); if (!res.ok) throw new Error('Server TTS failed'); const blob = await res.blob(); await new Promise(resolve => { const url = URL.createObjectURL(blob); const audio = new Audio(url); audio.onended = () => { URL.revokeObjectURL(url); setTimeout(resolve, 120); }; audio.onerror = () => { URL.revokeObjectURL(url); setTimeout(resolve, 120); }; audio.play(); }); } } /** Public controls */ function pauseTranscriptTTS(){ try{ speechSynthesis.pause(); ttsPaused = true; }catch{} } function resumeTranscriptTTS(){ try{ speechSynthesis.resume(); ttsPaused = false; }catch{} } function stopTranscriptTTS(silent=false){ try{ speechSynthesis.cancel(); }catch{} ttsPaused = false; ttsUtterances = []; if (!silent) {/* noop */} } /** Main entry: follows user’s selected language, voice, rate, pitch, engine. */ async function playTranscriptTTS() { const textEl = document.getElementById('transcriptContent'); if (!textEl) return alert('Transcript element not found.'); const text = textEl.textContent.trim(); if (!text) return alert('No transcript to play.'); stopTranscriptTTS(true); const prefs = getTTSPrefs(); // <- read from your popup/menu/localStorage const chunks = buildTranscriptChunks(text); try { if (prefs.engine === 'server') { // if you have server engine wired per language/voice await speakChunksServer(chunks, prefs); } else { await speakChunksBrowser(chunks, prefs); } } catch (e) { console.warn('TTS failed, trying browser fallback…', e); try { await speakChunksBrowser(chunks, prefs); } catch {} } } // // ---- Transcript TTS (male-ish, slow, resonant) ---- // let ttsUtterances = []; // let ttsPaused = false; // function buildTranscriptChunks(text){ // // Split on Sanskrit danda, punctuation, comma, and pipe with spaces optionally after // const parts = text.split(/[।॥.!?,|]\s*/).filter(Boolean); // // Merge into ~180-220 char chunks for natural pacing // const chunks = []; // let buffer = ""; // for (const p of parts){ // if ((buffer + " " + p).length > 200){ // if (buffer) chunks.push(buffer.trim()); // buffer = p; // } else buffer = (buffer ? buffer + " " : "") + p; // } // if (buffer) chunks.push(buffer.trim()); // return chunks; // } // function playTranscriptTTS(){ // const text = document.getElementById('transcriptContent').textContent.trim(); // if (!text){ alert("No transcript to play."); return; } // stopTranscriptTTS(true); // clear any existing // const rate = parseFloat(document.getElementById('ttsRate').value || "0.85"); // const pitch = parseFloat(document.getElementById('ttsPitch').value || "0.8"); // const voice = pickVoice(); // const chunks = buildTranscriptChunks(text); // ttsUtterances = chunks.map(chunk=>{ // const u = new SpeechSynthesisUtterance(chunk); // u.voice = voice; // u.lang = (voice?.lang || "hi-IN"); // u.rate = rate; // slower // u.pitch = pitch; // slightly deeper // u.volume = 1.0; // loud // // add small pause between chunks // u.onend = ()=>{ if (ttsUtterances.length) setTimeout(()=>{}, 250); }; // return u; // }); // // Some browsers need async voice load // const startSpeaking = () => { // // Chain play // function speakNext(){ // if (!ttsUtterances.length) return; // const u = ttsUtterances.shift(); // u.onend = ()=>{ if (!ttsPaused) speakNext(); }; // try { speechSynthesis.speak(u); } catch {} // } // speakNext(); // }; // if (speechSynthesis.getVoices().length === 0) { // speechSynthesis.onvoiceschanged = () => startSpeaking(); // } else { // startSpeaking(); // } // } // function pauseTranscriptTTS(){ try{ speechSynthesis.pause(); ttsPaused = true; }catch{} } // function resumeTranscriptTTS(){ try{ speechSynthesis.resume(); ttsPaused = false; }catch{} } // function stopTranscriptTTS(silent=false){ // try{ speechSynthesis.cancel(); }catch{} // ttsPaused = false; ttsUtterances = []; // if (!silent) { /* noop */ } // } // Logout helper (if you ever need) function logout(){ localStorage.removeItem("authToken"); window.location.href = "login.html"; }