// === 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"; }