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
@@ -271,6 +271,7 @@ async fn ensure_one(
username: session.username,
password: session.password,
library_bulk: true,
library_server_id: Some(session.library_server_id),
};
let _ = CoverCacheState::ensure_inner(&st, &app, &args, Some(http_sem)).await;
}
+41 -1
View File
@@ -107,6 +107,10 @@ pub struct CoverCacheEnsureArgs {
/// Library backfill: all derived tiers, no `cover:tier-ready` floods to the webview.
#[serde(default)]
pub library_bulk: bool,
/// Library server id (DB key) — set by backfill so a failed fetch can be logged
/// with the album/artist name. On-demand UI ensures leave it `None`.
#[serde(default)]
pub library_server_id: Option<String>,
}
fn cover_dir_for_args(root: &Path, args: &CoverCacheEnsureArgs) -> PathBuf {
@@ -266,7 +270,8 @@ impl CoverCacheState {
} else {
match download_cover_payload(&dir, &client, &http_sem, args).await {
Ok(bytes) => CoverSource::Bytes(bytes),
Err(_) => {
Err(err) => {
log_cover_fetch_failure(app, args, &err);
let _ = std::fs::create_dir_all(&dir);
let _ = std::fs::write(dir.join(COVER_FETCH_FAIL_MARKER), b"1");
return Ok(CoverCacheEnsureResult {
@@ -359,6 +364,41 @@ impl CoverCacheState {
}
}
/// Log a non-200 / failed cover download with the album/artist name when known.
/// Backfill fetches (`library_bulk`) log at the normal level — the user wants to
/// see which covers a busy server refused; incidental on-demand UI misses stay at
/// the debug level so they don't spam the normal log.
fn log_cover_fetch_failure(app: &AppHandle, args: &CoverCacheEnsureArgs, err: &str) {
let label = args
.library_server_id
.as_deref()
.filter(|s| !s.is_empty())
.and_then(|lib_id| {
app.try_state::<LibraryRuntime>().and_then(|rt| {
psysonic_library::cover_resolve::describe_cover_entity(
&rt.store,
lib_id,
&args.cache_kind,
&args.cache_entity_id,
)
})
})
.unwrap_or_else(|| format!("{} {}", args.cache_kind, args.cache_entity_id));
if args.library_bulk {
crate::app_eprintln!(
"[cover-backfill] fetch failed for {label} (coverArtId={}, tier={}): {err}",
args.cover_art_id,
args.tier
);
} else {
crate::app_deprintln!(
"[cover] fetch failed for {label} (coverArtId={}, tier={}): {err}",
args.cover_art_id,
args.tier
);
}
}
fn emit_tier_ready(app: &AppHandle, args: &CoverCacheEnsureArgs, tier: u32, path: &Path) {
let Ok(meta) = std::fs::metadata(path) else {
return;