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:
cucadmuh
2026-06-02 10:58:39 +03:00
committed by GitHub
parent a63ba3c9cb
commit 42aec6720c
7 changed files with 244 additions and 17 deletions
+24 -6
View File
@@ -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`. */