mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
fix(statistics): accurate counts for large libraries + album-artist tooltip
Remove the 10-page cap in fetchStatisticsLibraryAggregates — the loop now runs until the server returns a partial page, so albums/songs/playtime reflect the full library regardless of size (previously capped at 5,000 albums). Switched sort type from 'newest' to 'alphabeticalByName' for a stable pagination order. Add a tooltip on the Artists stat card explaining it shows album artists only (Subsonic API limitation — track-level featured/guest artists without their own album are not included). Tooltip added in all 8 locales. Labels with a tooltip get a dotted underline and cursor:help as visual hint. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+4
-9
@@ -631,11 +631,10 @@ export async function fetchStatisticsLibraryAggregates(): Promise<StatisticsLibr
|
|||||||
let songsCounted = 0;
|
let songsCounted = 0;
|
||||||
const genreAgg = new Map<string, { songCount: number; albumCount: number }>();
|
const genreAgg = new Map<string, { songCount: number; albumCount: number }>();
|
||||||
const pageSize = 500;
|
const pageSize = 500;
|
||||||
const maxPages = 10;
|
const capped = false;
|
||||||
let capped = false;
|
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
let nextPage = getAlbumList('newest', pageSize, 0);
|
let nextPage = getAlbumList('alphabeticalByName', pageSize, 0);
|
||||||
for (let page = 0; page < maxPages; page++) {
|
for (;;) {
|
||||||
try {
|
try {
|
||||||
const albums = await nextPage;
|
const albums = await nextPage;
|
||||||
for (const a of albums) {
|
for (const a of albums) {
|
||||||
@@ -653,12 +652,8 @@ export async function fetchStatisticsLibraryAggregates(): Promise<StatisticsLibr
|
|||||||
g.albumCount += 1;
|
g.albumCount += 1;
|
||||||
}
|
}
|
||||||
if (albums.length < pageSize) break;
|
if (albums.length < pageSize) break;
|
||||||
if (page === maxPages - 1) {
|
|
||||||
capped = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
offset += pageSize;
|
offset += pageSize;
|
||||||
nextPage = getAlbumList('newest', pageSize, offset);
|
nextPage = getAlbumList('alphabeticalByName', pageSize, offset);
|
||||||
} catch {
|
} catch {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -785,6 +785,7 @@ export const deTranslation = {
|
|||||||
genreDistribution: 'Genre-Verteilung (Top 20)',
|
genreDistribution: 'Genre-Verteilung (Top 20)',
|
||||||
loadMore: 'Mehr laden',
|
loadMore: 'Mehr laden',
|
||||||
statArtists: 'Künstler',
|
statArtists: 'Künstler',
|
||||||
|
statArtistsTooltip: 'Nur Album-Künstler — Künstler, die ausschließlich als Track-Künstler vorkommen (Featured, Gast usw.) und kein eigenes Album haben, werden nicht gezählt.',
|
||||||
statAlbums: 'Alben',
|
statAlbums: 'Alben',
|
||||||
statSongs: 'Songs',
|
statSongs: 'Songs',
|
||||||
statGenres: 'Genres',
|
statGenres: 'Genres',
|
||||||
|
|||||||
@@ -787,6 +787,7 @@ export const enTranslation = {
|
|||||||
genreDistribution: 'Genre Distribution (Top 20)',
|
genreDistribution: 'Genre Distribution (Top 20)',
|
||||||
loadMore: 'Load more',
|
loadMore: 'Load more',
|
||||||
statArtists: 'Artists',
|
statArtists: 'Artists',
|
||||||
|
statArtistsTooltip: 'Album artists only — artists appearing only as a track-level artist (featured, guest, etc.) without their own album are not included.',
|
||||||
statAlbums: 'Albums',
|
statAlbums: 'Albums',
|
||||||
statSongs: 'Songs',
|
statSongs: 'Songs',
|
||||||
statGenres: 'Genres',
|
statGenres: 'Genres',
|
||||||
|
|||||||
@@ -774,6 +774,7 @@ export const esTranslation = {
|
|||||||
genreDistribution: 'Distribución de Géneros (Top 20)',
|
genreDistribution: 'Distribución de Géneros (Top 20)',
|
||||||
loadMore: 'Cargar más',
|
loadMore: 'Cargar más',
|
||||||
statArtists: 'Artistas',
|
statArtists: 'Artistas',
|
||||||
|
statArtistsTooltip: 'Solo artistas de álbum — los artistas que aparecen únicamente como artistas de pista (featuring, invitado, etc.) sin álbum propio no se cuentan.',
|
||||||
statAlbums: 'Álbumes',
|
statAlbums: 'Álbumes',
|
||||||
statSongs: 'Canciones',
|
statSongs: 'Canciones',
|
||||||
statGenres: 'Géneros',
|
statGenres: 'Géneros',
|
||||||
|
|||||||
@@ -783,6 +783,7 @@ export const frTranslation = {
|
|||||||
genreDistribution: 'Répartition par genre (Top 20)',
|
genreDistribution: 'Répartition par genre (Top 20)',
|
||||||
loadMore: 'Charger plus',
|
loadMore: 'Charger plus',
|
||||||
statArtists: 'Artistes',
|
statArtists: 'Artistes',
|
||||||
|
statArtistsTooltip: 'Artistes d\'album uniquement — les artistes apparaissant seulement sur des pistes (featuring, invité, etc.) sans album propre ne sont pas comptés.',
|
||||||
statAlbums: 'Albums',
|
statAlbums: 'Albums',
|
||||||
statSongs: 'Morceaux',
|
statSongs: 'Morceaux',
|
||||||
statGenres: 'Genres',
|
statGenres: 'Genres',
|
||||||
|
|||||||
@@ -782,6 +782,7 @@ export const nbTranslation = {
|
|||||||
genreDistribution: 'Sjangerfordeling (Topp 20)',
|
genreDistribution: 'Sjangerfordeling (Topp 20)',
|
||||||
loadMore: 'Last inn mer',
|
loadMore: 'Last inn mer',
|
||||||
statArtists: 'Artister',
|
statArtists: 'Artister',
|
||||||
|
statArtistsTooltip: 'Kun albumartister — artister som bare opptrer som sporartist (featuring, gjest, osv.) uten eget album telles ikke med.',
|
||||||
statAlbums: 'Album',
|
statAlbums: 'Album',
|
||||||
statSongs: 'Sanger',
|
statSongs: 'Sanger',
|
||||||
statGenres: 'Sjangere',
|
statGenres: 'Sjangere',
|
||||||
|
|||||||
@@ -783,6 +783,7 @@ export const nlTranslation = {
|
|||||||
genreDistribution: 'Genreverdeling (Top 20)',
|
genreDistribution: 'Genreverdeling (Top 20)',
|
||||||
loadMore: 'Meer laden',
|
loadMore: 'Meer laden',
|
||||||
statArtists: 'Artiesten',
|
statArtists: 'Artiesten',
|
||||||
|
statArtistsTooltip: 'Alleen albumartiesten — artiesten die enkel als trackartiest voorkomen (featuring, gast, enz.) zonder eigen album worden niet meegeteld.',
|
||||||
statAlbums: 'Albums',
|
statAlbums: 'Albums',
|
||||||
statSongs: 'Nummers',
|
statSongs: 'Nummers',
|
||||||
statGenres: 'Genres',
|
statGenres: 'Genres',
|
||||||
|
|||||||
@@ -834,6 +834,7 @@ export const ruTranslation = {
|
|||||||
genreDistribution: 'Жанры (топ-20)',
|
genreDistribution: 'Жанры (топ-20)',
|
||||||
loadMore: 'Ещё',
|
loadMore: 'Ещё',
|
||||||
statArtists: 'Исполнители',
|
statArtists: 'Исполнители',
|
||||||
|
statArtistsTooltip: 'Только исполнители альбомов — артисты, присутствующие лишь в треках (фичеринг, гость и т. д.) без собственного альбома, не учитываются.',
|
||||||
statAlbums: 'Альбомы',
|
statAlbums: 'Альбомы',
|
||||||
statSongs: 'Треки',
|
statSongs: 'Треки',
|
||||||
statGenres: 'Жанры',
|
statGenres: 'Жанры',
|
||||||
|
|||||||
@@ -779,6 +779,7 @@ export const zhTranslation = {
|
|||||||
genreDistribution: '流派分布(前 20)',
|
genreDistribution: '流派分布(前 20)',
|
||||||
loadMore: '加载更多',
|
loadMore: '加载更多',
|
||||||
statArtists: '艺术家',
|
statArtists: '艺术家',
|
||||||
|
statArtistsTooltip: '仅限专辑艺术家——仅作为单曲艺术家出现(合唱、客串等)且无自己专辑的艺术家不计入此处。',
|
||||||
statAlbums: '专辑',
|
statAlbums: '专辑',
|
||||||
statSongs: '歌曲',
|
statSongs: '歌曲',
|
||||||
statGenres: '流派',
|
statGenres: '流派',
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ export default function Statistics() {
|
|||||||
n === null ? t('statistics.computing') : (playtimeCapped ? '≥ ' : '') + n.toLocaleString();
|
n === null ? t('statistics.computing') : (playtimeCapped ? '≥ ' : '') + n.toLocaleString();
|
||||||
|
|
||||||
const stats = [
|
const stats = [
|
||||||
{ label: t('statistics.statArtists'), value: artistCount?.toLocaleString() ?? '—' },
|
{ label: t('statistics.statArtists'), value: artistCount?.toLocaleString() ?? '—', tooltip: t('statistics.statArtistsTooltip') },
|
||||||
{ label: t('statistics.statAlbums'), value: countDisplay(totalAlbums) },
|
{ label: t('statistics.statAlbums'), value: countDisplay(totalAlbums) },
|
||||||
{ label: t('statistics.statSongs'), value: countDisplay(totalSongs) },
|
{ label: t('statistics.statSongs'), value: countDisplay(totalSongs) },
|
||||||
{ label: t('statistics.statPlaytime'), value: playtimeDisplay },
|
{ label: t('statistics.statPlaytime'), value: playtimeDisplay },
|
||||||
@@ -183,7 +183,7 @@ export default function Statistics() {
|
|||||||
{stats.map(s => (
|
{stats.map(s => (
|
||||||
<div key={s.label} className="stats-card">
|
<div key={s.label} className="stats-card">
|
||||||
<span className="stats-card-value">{s.value}</span>
|
<span className="stats-card-value">{s.value}</span>
|
||||||
<span className="stats-card-label">{s.label}</span>
|
<span className="stats-card-label" data-tooltip={s.tooltip} data-tooltip-wrap="true">{s.label}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3982,6 +3982,13 @@ html.no-compositing .fs-lyrics-rail {
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stats-card-label[data-tooltip] {
|
||||||
|
cursor: help;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-style: dotted;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Genre chart */
|
/* Genre chart */
|
||||||
.genre-chart {
|
.genre-chart {
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
|
|||||||
Reference in New Issue
Block a user