mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
fix(cover): stop per-song over-fetch + log failed cover downloads (#944)
* fix(cover): stop per-song cover over-fetch (album/mf-* explosion) album_has_distinct_disc_covers returned true as soon as two tracks on the same disc had different cover ids. On Navidrome every song has its own mf-<id> coverArt, so almost every album was flagged "distinct disc covers" and backfill warmed one cover per track (~520k for ~170k tracks), filling album/ with mf-* dirs instead of ~one cover per album. Treat a release as having distinct disc covers only when each disc has a single consistent cover that differs across discs (genuine box set); per-song ids now collapse to one cover per album. Mirror the same fix in the TS albumHasDistinctDiscCovers used by on-demand warming. Adds regression tests on both sides. * docs(changelog): record per-song cover over-fetch fix (PR #944) Give the album/mf-* over-fetch fix its own [1.47.0] Fixed entry and a settingsCredits line under PR #944. * feat(cover): log failed cover downloads with album/artist name A non-200 (or network-failed) getCoverArt download was swallowed silently. Now the failure is logged with the resolved album/artist name and the server error, so a server refusing covers under backfill load (5xx/429/timeouts) is visible. - cover_resolve: describe_cover_entity() resolves a human label from the local index (album "Name" — Artist / artist "Name"), best-effort with id fallback. - cover_cache: log_cover_fetch_failure() in ensure_inner logs on the download error path; threads optional library_server_id through CoverCacheEnsureArgs so the name lookup happens only on failure. Backfill logs at normal level, on-demand misses at debug level.
This commit is contained in:
@@ -145,6 +145,7 @@ const CONTRIBUTOR_ENTRIES = [
|
||||
'Live Search: scoped browse on Artists, Albums, New Releases, Tracks, and Composers — header badge, ghost restore, album title FTS, session stash (PR #938)',
|
||||
'Performance: idle Rust CPU — backfill coordinator park, probe overlay stability, throttled idle polls, lazy cover prefetch restore (PR #939)',
|
||||
'Cover backfill: disk-free idle gate, snapshot-diff worklist, live-tunable parallelism, transient-error retries, and memoized offline & cache stats to stop idle CPU spin (PR #943)',
|
||||
'Cover art: fix per-song cover over-fetch on Navidrome — only genuine per-disc artwork expands, collapsing ~520k per-track fetches to one cover per album (PR #944)',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -58,4 +58,25 @@ describe('albumHasDistinctDiscCovers', () => {
|
||||
]),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('false for per-song ids within a single disc (Navidrome)', () => {
|
||||
expect(
|
||||
albumHasDistinctDiscCovers([
|
||||
{ id: 't1', albumId: 'al-1', coverArt: 'mf-1', discNumber: 1 },
|
||||
{ id: 't2', albumId: 'al-1', coverArt: 'mf-2', discNumber: 1 },
|
||||
{ id: 't3', albumId: 'al-1', coverArt: 'mf-3', discNumber: 1 },
|
||||
]),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('false for per-song ids across discs (no shared disc cover)', () => {
|
||||
expect(
|
||||
albumHasDistinctDiscCovers([
|
||||
{ id: 't1', albumId: 'al-1', coverArt: 'mf-1', discNumber: 1 },
|
||||
{ id: 't2', albumId: 'al-1', coverArt: 'mf-2', discNumber: 1 },
|
||||
{ id: 't3', albumId: 'al-1', coverArt: 'mf-3', discNumber: 2 },
|
||||
{ id: 't4', albumId: 'al-1', coverArt: 'mf-4', discNumber: 2 },
|
||||
]),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,21 +33,39 @@ export function resolveSongFetchCoverArtId(song: CoverArtResolvableSong): string
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** True when 2+ discs use different cover art ids. */
|
||||
/**
|
||||
* True only for genuine per-disc artwork: a multi-disc release where each disc
|
||||
* has ONE consistent cover and those covers differ between discs (e.g. a box
|
||||
* set). It must NOT be tripped by per-song cover ids — Navidrome (and other
|
||||
* OpenSubsonic servers) give every track its own `mf-<id>` coverArt, so a disc
|
||||
* whose tracks carry many different ids is per-song art, not per-disc art, and
|
||||
* treating it as distinct would warm one cover per track instead of per album.
|
||||
*
|
||||
* Mirrors `album_has_distinct_disc_covers` in `psysonic-library/cover_resolve.rs`.
|
||||
*/
|
||||
export function albumHasDistinctDiscCovers(
|
||||
songs: ReadonlyArray<Pick<SubsonicSong, 'discNumber' | 'coverArt' | 'id' | 'albumId'>>,
|
||||
): boolean {
|
||||
const artByDisc = new Map<number, string>();
|
||||
const artByDisc = new Map<number, Set<string>>();
|
||||
for (const song of songs) {
|
||||
const disc = song.discNumber ?? 1;
|
||||
const artId = resolveSongFetchCoverArtId(song);
|
||||
if (!artId) continue;
|
||||
const prev = artByDisc.get(disc);
|
||||
if (prev !== undefined && prev !== artId) return true;
|
||||
artByDisc.set(disc, artId);
|
||||
let set = artByDisc.get(disc);
|
||||
if (!set) {
|
||||
set = new Set<string>();
|
||||
artByDisc.set(disc, set);
|
||||
}
|
||||
set.add(artId);
|
||||
}
|
||||
if (artByDisc.size <= 1) return false;
|
||||
return new Set(artByDisc.values()).size > 1;
|
||||
const discCovers = new Set<string>();
|
||||
for (const covers of artByDisc.values()) {
|
||||
// Tracks within a disc disagree → per-song ids, not a shared disc cover.
|
||||
if (covers.size !== 1) return false;
|
||||
for (const cover of covers) discCovers.add(cover);
|
||||
}
|
||||
return discCovers.size > 1;
|
||||
}
|
||||
|
||||
/** Album entity — one cache slot per album unless `distinctDiscCovers`. */
|
||||
|
||||
Reference in New Issue
Block a user