import React, { useState, useRef, useEffect, useCallback, memo } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Music, Star, ExternalLink, MicVocal, Heart, Cast, Users, Radio, Clock, SkipForward } from 'lucide-react'; import { usePlayerStore } from '../store/playerStore'; import { useAuthStore } from '../store/authStore'; import { useLyricsStore } from '../store/lyricsStore'; import { buildCoverArtUrl, coverArtCacheKey, getSong, star, unstar, getAlbum, getArtistInfo, SubsonicSong, SubsonicArtistInfo, } from '../api/subsonic'; import { useCachedUrl } from '../components/CachedImage'; import { useRadioMetadata } from '../hooks/useRadioMetadata'; // ─── Helpers ────────────────────────────────────────────────────────────────── function formatTime(s: number): string { if (!s || isNaN(s)) return '0:00'; const m = Math.floor(s / 60); return `${m}:${Math.floor(s % 60).toString().padStart(2, '0')}`; } function sanitizeHtml(html: string): string { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); doc.querySelectorAll('script, style, iframe, object, embed, form, input, button, select, base, meta, link').forEach(el => el.remove()); doc.querySelectorAll('*').forEach(el => { Array.from(el.attributes).forEach(attr => { const name = attr.name.toLowerCase(); const val = attr.value.toLowerCase().trim(); if (name.startsWith('on') || (name === 'href' && (val.startsWith('javascript:') || val.startsWith('data:'))) || (name === 'src' && (val.startsWith('javascript:') || val.startsWith('data:')))) { el.removeAttribute(attr.name); } }); }); return doc.body.innerHTML; } function renderStars(rating?: number) { if (!rating) return null; return (
{t('nowPlaying.nothingPlaying')}