/* ── Cadence dashboard ───────────────────────────────────────────────────── Views: overview · search · trending · browse (genres/moods) · artist · album playlist · recent Engine stats + buffer-ahead telemetry. ──────────────────────────────────────────────────────────────────────── */ (() => { 'use strict'; const $ = (s, r = document) => r.querySelector(s); const audio = $('#audio'); const main = $('#main'); /* ── state ──────────────────────────────────────────────────────────────── */ const state = { queue: [], queueSource: null, index: -1, current: null, shuffle: false, repeat: 0, queueLabel: null, station: null, growing: false, suggestionsAdded: 0, viewLists: {}, artistCache: {}, country: localStorage.getItem('cadence:country') || 'CH', token: 0, buffered: { id: null, ready: false, bytes: 0 }, }; const LABELS = { search: 'Search results', top: 'Top songs', album: 'Album', recent: 'Recently played', playlist: 'Playlist', trending: 'Trending', over_rec: 'Recently played', feed: 'Suggestions' }; const LS = { get(k, d) { try { return JSON.parse(localStorage.getItem('cadence:' + k)) ?? d; } catch { return d; } }, set(k, v) { try { localStorage.setItem('cadence:' + k, JSON.stringify(v)); } catch {} }, }; /* ── helpers ────────────────────────────────────────────────────────────── */ const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); const icon = n => ``; const thumb = vid => `/api/thumb/${vid}`; const imgp = u => `/api/img?u=${encodeURIComponent(u)}`; const artistName = t => (t && (t.artist_text || 'Unknown artist')) || 'Unknown artist'; function fmtTime(sec) { if (!sec || !isFinite(sec) || sec < 0) return '0:00'; sec = Math.floor(sec); const h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = sec % 60; return h ? `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` : `${m}:${String(s).padStart(2, '0')}`; } const fmtBytes = b => { if (!b) return '0 B'; const u = ['B', 'KB', 'MB', 'GB']; const i = Math.min(Math.floor(Math.log(b) / Math.log(1024)), 3); return `${(b / 1024 ** i).toFixed(i ? 1 : 0)} ${u[i]}`; }; const fmtDur = s => { s = Math.round(s || 0); const m = Math.floor(s / 60); return m >= 60 ? `${Math.floor(m / 60)}h ${m % 60}m` : `${m}m ${s % 60}s`; }; function toast(msg, isErr = false, ms = 3200) { const el = document.createElement('div'); el.className = 'toast' + (isErr ? ' err' : ''); el.textContent = msg; $('#toasts').appendChild(el); setTimeout(() => { el.style.opacity = '0'; el.style.transition = 'opacity .3s'; setTimeout(() => el.remove(), 320); }, ms); } async function api(path, opts) { const r = await fetch(path, opts); let data = null; try { data = await r.json(); } catch {} if (!r.ok) { const err = new Error((data && data.error) || `Request failed (${r.status})`); err.status = r.status; err.code = data && data.code; err.hint = data && data.hint; throw err; } return data; } // A dead