import { getAlbumList, getAlbum } from '../api/subsonicLibrary'; import type { SubsonicAlbum } from '../api/subsonicTypes'; import { songToTrack } from '../utils/playback/songToTrack'; import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { ArrowUpDown, ArrowDown, ArrowUp, TrendingUp, UsersRound, Play, ListPlus } from 'lucide-react'; import { useAuthStore } from '../store/authStore'; import { usePlayerStore } from '../store/playerStore'; import { AlbumCoverArtImage } from '../cover/AlbumCoverArtImage'; import { ArtistCoverArtImage } from '../cover/ArtistCoverArtImage'; import { playAlbum } from '../utils/playback/playAlbum'; import { useTranslation } from 'react-i18next'; const PAGE_SIZE = 50; interface ArtistEntry { id: string; name: string; coverArt?: string; totalPlays: number; } const COMPILATION_NAMES = new Set([ 'various artists', 'various', 'va', 'v.a.', 'v.a', 'diverse artister', 'diversos artistas', 'artistes variés', 'vários artistas', 'verschiedene künstler', 'verscheidene artiesten', 'compilations', 'soundtrack', 'original soundtrack', 'ost', 'original motion picture soundtrack', 'original score', ]); function isCompilation(name: string): boolean { return COMPILATION_NAMES.has(name.toLowerCase().trim()); } function deriveTopArtists(albums: SubsonicAlbum[], filterCompilations: boolean): ArtistEntry[] { const map = new Map(); for (const a of albums) { const plays = a.playCount ?? 0; if (plays === 0) continue; if (filterCompilations && isCompilation(a.artist ?? '')) continue; const entry = map.get(a.artistId); if (entry) { entry.totalPlays += plays; if (!entry.coverArt && a.coverArt) entry.coverArt = a.coverArt; } else { map.set(a.artistId, { id: a.artistId, name: a.artist, coverArt: a.coverArt, totalPlays: plays }); } } return [...map.values()].sort((a, b) => b.totalPlays - a.totalPlays); } function formatPlays(n: number, t: ReturnType['t']): string { return t('mostPlayed.plays', { n: n.toLocaleString() }) as string; } /** Most-played list row cover layout px. */ const MOST_PLAYED_COVER_CSS_PX = 80; export default function MostPlayed() { const { t } = useTranslation(); const navigate = useNavigate(); const musicLibraryFilterVersion = useAuthStore(s => s.musicLibraryFilterVersion); const openContextMenu = usePlayerStore(s => s.openContextMenu); const enqueue = usePlayerStore(s => s.enqueue); const handleEnqueueAlbum = useCallback(async (albumId: string) => { try { const data = await getAlbum(albumId); enqueue(data.songs.map(songToTrack)); } catch { // Network failure — silent (toast would be too noisy for a hover action). } }, [enqueue]); const [albums, setAlbums] = useState([]); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [hasMore, setHasMore] = useState(true); const [sortAsc, setSortAsc] = useState(false); // false = most plays first const [filterCompilations, setFilterCompilations] = useState(false); const topArtists = deriveTopArtists(albums, filterCompilations).slice(0, 10); const load = useCallback(async () => { setLoading(true); setAlbums([]); setHasMore(true); try { const result = await getAlbumList('frequent', PAGE_SIZE, 0); setAlbums(result); setHasMore(result.length === PAGE_SIZE); } catch {} setLoading(false); }, [musicLibraryFilterVersion]); useEffect(() => { load(); }, [load]); const loadMore = async () => { if (loadingMore || !hasMore) return; setLoadingMore(true); try { const result = await getAlbumList('frequent', PAGE_SIZE, albums.length); setAlbums(prev => [...prev, ...result]); setHasMore(result.length === PAGE_SIZE); } catch {} setLoadingMore(false); }; const sorted = sortAsc ? [...albums].reverse() : albums; const withPlays = sorted.filter(a => (a.playCount ?? 0) > 0); return (

{t('mostPlayed.title')}

{/* ── Top Artists ── */} {!loading && (

{t('mostPlayed.topArtists')}

{topArtists.length === 0 && (
{t('mostPlayed.noArtists')}
)}
{topArtists.map((artist, i) => ( ))}
)} {/* ── Top Albums ── */}

{t('mostPlayed.topAlbums')}

{loading ? (
) : withPlays.length === 0 ? (
{t('mostPlayed.noData')}
) : ( <>
{withPlays.map((album, i) => (
navigate(`/album/${album.id}`)} onContextMenu={e => { e.preventDefault(); openContextMenu(e.clientX, e.clientY, album, 'album'); }} > {sortAsc ? withPlays.length - i : i + 1} {album.coverArt ? ( ) : (
)}
{album.name} {t('mostPlayed.plays', { n: (album.playCount ?? 0).toLocaleString() })}
{ e.stopPropagation(); navigate(`/artist/${album.artistId}`); }} > {album.artist}
{album.year && {album.year}}
))}
{hasMore && ( )} )}
); }