import React, { useEffect, useState } from 'react'; import { getAlbumList, getArtists, getGenres, SubsonicAlbum } from '../api/subsonic'; import AlbumRow from '../components/AlbumRow'; import { useTranslation } from 'react-i18next'; import { useAuthStore } from '../store/authStore'; import { lastfmIsConfigured, lastfmGetTopArtists, lastfmGetTopAlbums, lastfmGetTopTracks, lastfmGetRecentTracks, LastfmPeriod, LastfmTopArtist, LastfmTopAlbum, LastfmTopTrack, LastfmRecentTrack } from '../api/lastfm'; function relativeTime(timestamp: number, t: (key: string, opts?: object) => string): string { const diff = Math.floor(Date.now() / 1000) - timestamp; if (diff < 60) return t('statistics.lfmJustNow'); if (diff < 3600) return t('statistics.lfmMinutesAgo', { n: Math.floor(diff / 60) }); if (diff < 86400) return t('statistics.lfmHoursAgo', { n: Math.floor(diff / 3600) }); return t('statistics.lfmDaysAgo', { n: Math.floor(diff / 86400) }); } const PERIODS: { key: LastfmPeriod; label: string }[] = [ { key: '7day', label: 'lfmPeriod7day' }, { key: '1month', label: 'lfmPeriod1month' }, { key: '3month', label: 'lfmPeriod3month' }, { key: '6month', label: 'lfmPeriod6month' }, { key: '12month', label: 'lfmPeriod12month' }, { key: 'overall', label: 'lfmPeriodOverall' }, ]; export default function Statistics() { const { t } = useTranslation(); const { lastfmSessionKey, lastfmUsername } = useAuthStore(); const [recent, setRecent] = useState([]); const [frequent, setFrequent] = useState([]); const [highest, setHighest] = useState([]); const [artistCount, setArtistCount] = useState(null); const [totalSongs, setTotalSongs] = useState(null); const [totalAlbums, setTotalAlbums] = useState(null); const [loading, setLoading] = useState(true); const [lfmPeriod, setLfmPeriod] = useState('1month'); const [lfmTopArtists, setLfmTopArtists] = useState([]); const [lfmTopAlbums, setLfmTopAlbums] = useState([]); const [lfmTopTracks, setLfmTopTracks] = useState([]); const [lfmLoading, setLfmLoading] = useState(false); const [lfmRecentTracks, setLfmRecentTracks] = useState([]); const [lfmRecentLoading, setLfmRecentLoading] = useState(false); useEffect(() => { Promise.all([ getAlbumList('recent', 20).catch(() => []), getAlbumList('frequent', 12).catch(() => []), getAlbumList('highest', 12).catch(() => []), getArtists().catch(() => []), getGenres().catch(() => []), ]).then(([rc, fr, hi, a, g]) => { setRecent(rc); setFrequent(fr); setHighest(hi); setArtistCount(a.length); setTotalSongs(g.reduce((acc: number, genre: any) => acc + genre.songCount, 0)); setTotalAlbums(g.reduce((acc: number, genre: any) => acc + genre.albumCount, 0)); setLoading(false); }).catch(() => setLoading(false)); }, []); useEffect(() => { if (!lastfmIsConfigured() || !lastfmSessionKey || !lastfmUsername) return; setLfmRecentLoading(true); lastfmGetRecentTracks(lastfmUsername, lastfmSessionKey, 20) .then(tracks => { setLfmRecentTracks(tracks); setLfmRecentLoading(false); }) .catch(() => setLfmRecentLoading(false)); }, [lastfmSessionKey, lastfmUsername]); useEffect(() => { if (!lastfmIsConfigured() || !lastfmSessionKey || !lastfmUsername) return; setLfmLoading(true); Promise.all([ lastfmGetTopArtists(lastfmUsername, lastfmSessionKey, lfmPeriod, 10), lastfmGetTopAlbums(lastfmUsername, lastfmSessionKey, lfmPeriod, 10), lastfmGetTopTracks(lastfmUsername, lastfmSessionKey, lfmPeriod, 10), ]).then(([artists, albums, tracks]) => { setLfmTopArtists(artists); setLfmTopAlbums(albums); setLfmTopTracks(tracks); setLfmLoading(false); }).catch(() => setLfmLoading(false)); }, [lfmPeriod, lastfmSessionKey, lastfmUsername]); const loadMore = async ( type: 'frequent' | 'highest', currentList: SubsonicAlbum[], setter: React.Dispatch> ) => { try { const more = await getAlbumList(type, 12, currentList.length); const newItems = more.filter(m => !currentList.find(c => c.id === m.id)); if (newItems.length > 0) setter(prev => [...prev, ...newItems]); } catch (e) { console.error('Failed to load more', e); } }; const stats = [ { label: t('statistics.statArtists'), value: artistCount }, { label: t('statistics.statAlbums'), value: totalAlbums }, { label: t('statistics.statSongs'), value: totalSongs }, ]; return (

{t('statistics.title')}

{loading ? (
) : (
{stats.map(s => (
{s.value?.toLocaleString() ?? '—'} {s.label}
))}
{recent.length > 0 && ( )} loadMore('frequent', frequent, setFrequent)} moreText={t('statistics.loadMore')} /> loadMore('highest', highest, setHighest)} moreText={t('statistics.loadMore')} /> {/* Last.fm Stats */} {lastfmIsConfigured() && (

{t('statistics.lfmTitle')}

{lastfmSessionKey && (
{PERIODS.map(p => ( ))}
)}
{!lastfmSessionKey ? (

{t('statistics.lfmNotConnected')}

) : lfmLoading ? (
) : (
{([ { label: t('statistics.lfmTopArtists'), items: lfmTopArtists.map(a => ({ primary: a.name, secondary: null, playcount: a.playcount })) }, { label: t('statistics.lfmTopAlbums'), items: lfmTopAlbums.map(a => ({ primary: a.name, secondary: a.artist, playcount: a.playcount })) }, { label: t('statistics.lfmTopTracks'), items: lfmTopTracks.map(tr => ({ primary: tr.name, secondary: tr.artist, playcount: tr.playcount })) }, ] as { label: string; items: { primary: string; secondary: string | null; playcount: string }[] }[]).map(col => { const max = Math.max(...col.items.map(it => Number(it.playcount)), 1); return (

{col.label}

    {col.items.map((it, i) => (
  1. {i + 1}
    {it.primary}
    {it.secondary && (
    {it.secondary}
    )}
    {Number(it.playcount).toLocaleString()}
  2. ))}
); })}
)}
)} {/* Recent Scrobbles */} {lastfmIsConfigured() && lastfmSessionKey && (

{t('statistics.lfmRecentTracks')}

{lfmRecentLoading ? (
) : (
{lfmRecentTracks.map((track, i) => (
{track.name} {track.nowPlaying && ( {t('statistics.lfmNowPlaying')} )}
{track.artist}{track.album ? ` · ${track.album}` : ''}
{track.nowPlaying ? '' : track.timestamp ? relativeTime(track.timestamp, t) : ''}
))}
)}
)}
)}
); }