Enrichment logic fixes

This commit is contained in:
2026-03-22 12:17:57 +00:00
parent 1976df2f52
commit d1a1f9bca5
3 changed files with 124 additions and 18 deletions

View File

@@ -67,6 +67,31 @@ while [[ $# -gt 0 ]]; do
FORCE=1
shift
;;
--help|-h)
cat <<'EOF'
Usage: scrbblr-publish [OPTIONS]
Generate and publish the HTML report only when new scrobbles exist.
Cover art is extracted automatically by `scrbblr report` before each run.
Options:
--output <dir> Directory for generated report files (default: ~/music-report)
--remote <target> rsync destination, e.g. user@host:/var/www/music-report
--db-path <path> Path to scrobbles database (default: auto-detected)
--watch Keep running, checking every --interval seconds
--interval <secs> Seconds between checks in --watch mode (default: 300)
--force Regenerate and publish even if no new scrobbles
--help, -h Show this help and exit
Config file (loaded before flags):
$XDG_CONFIG_HOME/scrbblr/publish.conf (primary)
~/.config/scrbblr/publish.conf (fallback)
~/.scrbblr-publish.conf (legacy)
Supported config variables: OUTPUT_DIR, REMOTE_TARGET, DB_PATH
EOF
exit 0
;;
*)
printf 'Unknown option: %s\n' "$1" >&2
exit 2

View File

@@ -143,6 +143,19 @@ enum Commands {
#[arg(long)]
all_time_limit: Option<i64>,
/// Skip the MPD cover extraction that normally runs before generating
/// the report. Useful if MPD is not running or you want a fast run.
#[arg(long)]
no_enrich: bool,
/// MPD host for cover extraction (default: localhost).
#[arg(long, default_value = "localhost")]
mpd_host: String,
/// MPD port for cover extraction (default: 6600).
#[arg(long, default_value = "6600")]
mpd_port: u16,
/// Path to the SQLite database file. Same default as `watch`.
#[arg(long)]
db_path: Option<String>,
@@ -473,11 +486,13 @@ fn round_to_5(n: i64) -> i64 {
/// Run the `report` subcommand.
///
/// Runs MPD cover extraction first (unless `--no-enrich`), then generates the report.
///
/// Three output modes:
/// - **Terminal** (default): queries a single `--period` and prints ASCII tables.
/// - **JSON** (`--json`): same single-period data as pretty-printed JSON.
/// - **HTML** (`--html`): generates a multi-period report (Today / Week / Month /
/// All Time) with bar charts and album cover art. Auto-runs enrichment first.
/// All Time) with bar charts and album cover art.
/// With `--output <dir>`, writes `index.html` + `covers/` to a directory.
fn run_report(
period: &str,
@@ -486,6 +501,8 @@ fn run_report(
output: Option<&str>,
limit: i64,
all_time_limit: i64,
no_enrich: bool,
mpd_cfg: &mpd::MpdConfig,
db_path: &str,
) {
let conn = match db::open_db(db_path) {
@@ -512,11 +529,25 @@ fn run_report(
std::process::exit(1);
}
// For HTML reports, enrich only the albums that will actually appear in
// the report (across all periods). Uses quiet mode so "nothing to do"
// isn't printed when everything is already cached.
if html {
// Cover enrichment is only meaningful for HTML reports — terminal and JSON
// output never displays cover art, so there is nothing to gain from
// extracting or downloading covers for those modes.
if html && !no_enrich {
// Determine exactly which (artist, album) pairs will appear in the
// HTML output across all periods. All enrichment is scoped to this set
// so we never fetch covers for albums that won't be shown.
let needed = report::albums_needed_for_report(&conn, limit, all_time_limit);
// Step 1: Extract embedded covers from MPD (fast, offline, no rate
// limits). Only processes albums in `needed` that still have no cover.
// Running this before the online step means CAA/iTunes won't waste a
// request on an album whose cover is already in the local music file.
mpd::run_mpd_cover_enrich_targeted(mpd_cfg, &conn, &needed);
// Step 2: Online enrichment for albums in `needed` that still have no
// cover after the MPD pass (e.g. streams, albums not in the MPD DB).
// Respects the 7-day cooldown so we don't hammer MusicBrainz on every
// report run.
enrich::run_enrich_targeted(&conn, &needed, true);
}
@@ -652,12 +683,19 @@ fn main() {
output,
limit,
all_time_limit,
no_enrich,
mpd_host,
mpd_port,
db_path,
} => {
let path = db_path.unwrap_or_else(default_db_path);
let atl =
all_time_limit.unwrap_or_else(|| round_to_5((limit as f64 * 2.5).round() as i64));
run_report(&period, json, html, output.as_deref(), limit, atl, &path);
let mpd_cfg = mpd::MpdConfig {
host: mpd_host,
port: mpd_port,
};
run_report(&period, json, html, output.as_deref(), limit, atl, no_enrich, &mpd_cfg, &path);
}
Commands::Enrich {
online,

View File

@@ -942,7 +942,8 @@ fn try_extract_cover(
/// one; if it finds nothing, the local cover is preserved (see
/// `db::upsert_album_cache` for the `COALESCE` logic).
pub fn run_mpd_cover_enrich(config: &MpdConfig, conn: &Connection) {
// Find all (artist, album) pairs with scrobbles but no cover yet.
// Fetch covers for every scrobbled album that still has no cover_url.
// Used by the standalone `enrich` command where the scope is the whole DB.
let albums = match db::albums_without_cover(conn) {
Ok(a) => a,
Err(e) => {
@@ -956,7 +957,54 @@ pub fn run_mpd_cover_enrich(config: &MpdConfig, conn: &Connection) {
return;
}
// Connect to MPD for this session.
run_mpd_cover_enrich_albums(config, conn, albums);
}
/// Targeted variant: only extract covers for albums that will actually appear
/// in the report. Avoids fetching covers for the entire DB when only a subset
/// is needed.
///
/// Used by `run_report` so that cover extraction is scoped to the albums the
/// HTML renderer will actually display.
pub fn run_mpd_cover_enrich_targeted(
config: &MpdConfig,
conn: &Connection,
needed: &std::collections::HashSet<(String, String)>,
) {
// Start from albums that genuinely have no cover yet, then narrow to the
// set the caller cares about. This avoids redundant MPD round-trips for
// albums that already have art.
let albums = match db::albums_without_cover(conn) {
Ok(a) => a,
Err(e) => {
eprintln!("[error] Failed to query albums without cover: {}", e);
return;
}
};
let albums: Vec<_> = albums
.into_iter()
.filter(|a| needed.contains(&(a.artist.clone(), a.album.clone())))
.collect();
if albums.is_empty() {
// Quiet: the caller knows this is a background step, no need to
// announce that nothing was missing.
return;
}
run_mpd_cover_enrich_albums(config, conn, albums);
}
/// Inner loop shared by `run_mpd_cover_enrich` and
/// `run_mpd_cover_enrich_targeted`. Connects to MPD and processes the given
/// album list.
fn run_mpd_cover_enrich_albums(
config: &MpdConfig,
conn: &Connection,
albums: Vec<db::UncachedAlbum>,
) {
// Connect to MPD once for the whole batch.
let mut mpd_conn = match connect(config) {
Ok(c) => c,
Err(e) => {
@@ -965,12 +1013,6 @@ pub fn run_mpd_cover_enrich(config: &MpdConfig, conn: &Connection) {
}
};
// Remove the read timeout for cover extraction — readpicture responses can
// be large and we don't need the idle-loop timeout here.
// We can't easily change the timeout on a Box<dyn Read>, so we just proceed
// with the existing short timeout but retry reads as needed. In practice,
// readpicture transfers are fast on a local socket.
let covers = enrich::covers_dir();
eprintln!(
"Extracting covers from MPD for {} album(s)...",
@@ -989,7 +1031,8 @@ pub fn run_mpd_cover_enrich(config: &MpdConfig, conn: &Connection) {
album.album
);
// Step 1: Ask MPD for a file from this album.
// Step 1: Ask MPD for any file from this album so we have a path to
// hand to readpicture.
let file_uri = match search_song_for_album(&mut mpd_conn, &album.artist, &album.album) {
Some(f) => f,
None => {
@@ -1010,8 +1053,8 @@ pub fn run_mpd_cover_enrich(config: &MpdConfig, conn: &Connection) {
}
};
// Step 3: Resize and re-encode the image to stay consistent with
// covers downloaded from the Cover Art Archive.
// Step 3: Resize and re-encode to stay consistent with covers
// downloaded from the Cover Art Archive.
let processed = match enrich::resize_cover_bytes(&picture_bytes) {
Some(b) => b,
None => {
@@ -1030,7 +1073,7 @@ pub fn run_mpd_cover_enrich(config: &MpdConfig, conn: &Connection) {
continue;
}
// Step 5: Record the local path in album_cache so the report generator
// Step 5: Record the local path in album_cache so the HTML renderer
// and the online enrich command can find it.
let cover_path = dest.to_string_lossy().to_string();
match db::set_local_cover(conn, &album.artist, &album.album, &cover_path) {