Enrichment logic fixes
This commit is contained in:
@@ -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
|
||||
|
||||
50
src/main.rs
50
src/main.rs
@@ -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,
|
||||
|
||||
67
src/mpd.rs
67
src/mpd.rs
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user