Cover repair for MPD

This commit is contained in:
2026-03-23 19:37:48 +00:00
parent ed5682c887
commit d199abdc68
4 changed files with 322 additions and 1 deletions

115
src/db.rs
View File

@@ -969,6 +969,46 @@ pub fn albums_without_cover_from_mpd(conn: &Connection) -> Result<Vec<UncachedAl
rows.collect()
}
/// A cached MPD-local cover entry keyed by `(artist, album)`.
///
/// `cover_url` is expected to point to a local file generated by MPD cover
/// extraction (`covers/mpd_<hash>.jpg`).
#[derive(Debug, Clone)]
pub struct MpdLocalCoverAlbum {
pub artist: String,
pub album: String,
pub cover_url: String,
}
/// Find MPD-sourced albums that currently point to a local MPD cover file.
///
/// Used by the `repair-mpd-covers` command to revalidate and, if needed,
/// re-fetch previously cached covers.
///
/// We only include rows where:
///
/// - at least one scrobble came from MPD (`source = 'MPD'`), and
/// - `album_cache.cover_url` is a non-NULL path containing `mpd_`.
pub fn albums_with_local_mpd_cover(conn: &Connection) -> Result<Vec<MpdLocalCoverAlbum>> {
let mut stmt = conn.prepare(
"SELECT DISTINCT s.artist, s.album, c.cover_url
FROM scrobbles s
JOIN album_cache c ON s.artist = c.artist AND s.album = c.album
WHERE s.source = 'MPD'
AND c.cover_url IS NOT NULL
AND c.cover_url LIKE '%mpd_%'
ORDER BY s.artist, s.album",
)?;
let rows = stmt.query_map([], |row| {
Ok(MpdLocalCoverAlbum {
artist: row.get(0)?,
album: row.get(1)?,
cover_url: row.get(2)?,
})
})?;
rows.collect()
}
/// Cached album metadata returned by [`album_cache_meta`].
#[derive(Debug, Clone)]
pub struct AlbumCacheMeta {
@@ -1761,6 +1801,81 @@ mod tests {
// set_local_cover
// -----------------------------------------------------------------------
#[test]
fn test_albums_with_local_mpd_cover_returns_only_mpd_local_rows() {
let conn = open_memory_db().unwrap();
insert_scrobble(
&conn,
&NewScrobble {
artist: "Deftones".into(),
album: "White Pony".into(),
title: "Digital Bath".into(),
track_duration_secs: Some(291),
played_duration_secs: 200,
scrobbled_at: today_at("10:00:00"),
source: "MPD".into(),
},
)
.unwrap();
insert_scrobble(
&conn,
&NewScrobble {
artist: "Tourist".into(),
album: "Inside Out".into(),
title: "Inside Out".into(),
track_duration_secs: Some(240),
played_duration_secs: 200,
scrobbled_at: today_at("10:05:00"),
source: "MPD".into(),
},
)
.unwrap();
insert_scrobble(
&conn,
&NewScrobble {
artist: "Bonobo".into(),
album: "Black Sands".into(),
title: "Kiara".into(),
track_duration_secs: Some(220),
played_duration_secs: 200,
scrobbled_at: today_at("10:10:00"),
source: "Qobuz".into(),
},
)
.unwrap();
set_local_cover(
&conn,
"Deftones",
"White Pony",
"/tmp/scrbblr/covers/mpd_deadbeef.jpg",
)
.unwrap();
set_local_cover(
&conn,
"Tourist",
"Inside Out",
"/tmp/scrbblr/covers/itunes_123.jpg",
)
.unwrap();
set_local_cover(
&conn,
"Bonobo",
"Black Sands",
"/tmp/scrbblr/covers/mpd_abcdef01.jpg",
)
.unwrap();
let rows = albums_with_local_mpd_cover(&conn).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].artist, "Deftones");
assert_eq!(rows[0].album, "White Pony");
assert!(rows[0].cover_url.contains("mpd_"));
}
#[test]
fn test_set_local_cover_inserts_new_row() {
let conn = open_memory_db().unwrap();