export function formatDuration(seconds) { if (!Number.isFinite(seconds) || seconds < 0) return '0:00'; const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return `${m}:${String(s).padStart(2, '0')}`; } export function formatDate(dateStr) { if (!dateStr) return ''; const date = new Date(dateStr); if (Number.isNaN(date.getTime())) return ''; return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); } export function formatYear(dateStr) { if (!dateStr) return ''; const date = new Date(dateStr); if (Number.isNaN(date.getTime())) return ''; return String(date.getFullYear()); } export function escapeHtml(value) { return String(value) .replace(/&/g, '\u0026amp;') .replace(//g, '\u0026gt;') .replace(/"/g, '\u0026quot;') .replace(/\u0027/g, '\u0026#39;'); } export function getTrackArtists(track) { if (!track || typeof track !== 'object') return 'Unknown artist'; if (track.artist?.name) return track.artist.name; if (Array.isArray(track.artists) && track.artists.length > 0) { return track.artists.map((a) => a?.name).filter(Boolean).join(', ') || 'Unknown artist'; } if (typeof track.artist === 'string' && track.artist.trim()) return track.artist; return 'Unknown artist'; } export function getTrackTitle(track) { return track?.title || 'Unknown title'; } export function sanitizeForFilename(str) { return String(str || '') .replace(/[\\/:*?"<>|]/g, '_') .replace(/\s+/g, ' ') .trim(); } export function buildTrackFilename(track, extension = 'flac') { const artists = sanitizeForFilename(getTrackArtists(track)); const title = sanitizeForFilename(getTrackTitle(track)); return `${artists} - ${title}.${extension}`; } export function debounce(fn, wait = 250) { let t = null; return (...args) => { if (t) clearTimeout(t); t = setTimeout(() => fn(...args), wait); }; }