Enrichment logic fixes
This commit is contained in:
@@ -67,6 +67,31 @@ while [[ $# -gt 0 ]]; do
|
|||||||
FORCE=1
|
FORCE=1
|
||||||
shift
|
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
|
printf 'Unknown option: %s\n' "$1" >&2
|
||||||
exit 2
|
exit 2
|
||||||
|
|||||||
50
src/main.rs
50
src/main.rs
@@ -143,6 +143,19 @@ enum Commands {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
all_time_limit: Option<i64>,
|
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`.
|
/// Path to the SQLite database file. Same default as `watch`.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
db_path: Option<String>,
|
db_path: Option<String>,
|
||||||
@@ -473,11 +486,13 @@ fn round_to_5(n: i64) -> i64 {
|
|||||||
|
|
||||||
/// Run the `report` subcommand.
|
/// Run the `report` subcommand.
|
||||||
///
|
///
|
||||||
|
/// Runs MPD cover extraction first (unless `--no-enrich`), then generates the report.
|
||||||
|
///
|
||||||
/// Three output modes:
|
/// Three output modes:
|
||||||
/// - **Terminal** (default): queries a single `--period` and prints ASCII tables.
|
/// - **Terminal** (default): queries a single `--period` and prints ASCII tables.
|
||||||
/// - **JSON** (`--json`): same single-period data as pretty-printed JSON.
|
/// - **JSON** (`--json`): same single-period data as pretty-printed JSON.
|
||||||
/// - **HTML** (`--html`): generates a multi-period report (Today / Week / Month /
|
/// - **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.
|
/// With `--output <dir>`, writes `index.html` + `covers/` to a directory.
|
||||||
fn run_report(
|
fn run_report(
|
||||||
period: &str,
|
period: &str,
|
||||||
@@ -486,6 +501,8 @@ fn run_report(
|
|||||||
output: Option<&str>,
|
output: Option<&str>,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
all_time_limit: i64,
|
all_time_limit: i64,
|
||||||
|
no_enrich: bool,
|
||||||
|
mpd_cfg: &mpd::MpdConfig,
|
||||||
db_path: &str,
|
db_path: &str,
|
||||||
) {
|
) {
|
||||||
let conn = match db::open_db(db_path) {
|
let conn = match db::open_db(db_path) {
|
||||||
@@ -512,11 +529,25 @@ fn run_report(
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For HTML reports, enrich only the albums that will actually appear in
|
// Cover enrichment is only meaningful for HTML reports — terminal and JSON
|
||||||
// the report (across all periods). Uses quiet mode so "nothing to do"
|
// output never displays cover art, so there is nothing to gain from
|
||||||
// isn't printed when everything is already cached.
|
// extracting or downloading covers for those modes.
|
||||||
if html {
|
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);
|
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);
|
enrich::run_enrich_targeted(&conn, &needed, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -652,12 +683,19 @@ fn main() {
|
|||||||
output,
|
output,
|
||||||
limit,
|
limit,
|
||||||
all_time_limit,
|
all_time_limit,
|
||||||
|
no_enrich,
|
||||||
|
mpd_host,
|
||||||
|
mpd_port,
|
||||||
db_path,
|
db_path,
|
||||||
} => {
|
} => {
|
||||||
let path = db_path.unwrap_or_else(default_db_path);
|
let path = db_path.unwrap_or_else(default_db_path);
|
||||||
let atl =
|
let atl =
|
||||||
all_time_limit.unwrap_or_else(|| round_to_5((limit as f64 * 2.5).round() as i64));
|
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 {
|
Commands::Enrich {
|
||||||
online,
|
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
|
/// one; if it finds nothing, the local cover is preserved (see
|
||||||
/// `db::upsert_album_cache` for the `COALESCE` logic).
|
/// `db::upsert_album_cache` for the `COALESCE` logic).
|
||||||
pub fn run_mpd_cover_enrich(config: &MpdConfig, conn: &Connection) {
|
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) {
|
let albums = match db::albums_without_cover(conn) {
|
||||||
Ok(a) => a,
|
Ok(a) => a,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -956,7 +957,54 @@ pub fn run_mpd_cover_enrich(config: &MpdConfig, conn: &Connection) {
|
|||||||
return;
|
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) {
|
let mut mpd_conn = match connect(config) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
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();
|
let covers = enrich::covers_dir();
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Extracting covers from MPD for {} album(s)...",
|
"Extracting covers from MPD for {} album(s)...",
|
||||||
@@ -989,7 +1031,8 @@ pub fn run_mpd_cover_enrich(config: &MpdConfig, conn: &Connection) {
|
|||||||
album.album
|
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) {
|
let file_uri = match search_song_for_album(&mut mpd_conn, &album.artist, &album.album) {
|
||||||
Some(f) => f,
|
Some(f) => f,
|
||||||
None => {
|
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
|
// Step 3: Resize and re-encode to stay consistent with covers
|
||||||
// covers downloaded from the Cover Art Archive.
|
// downloaded from the Cover Art Archive.
|
||||||
let processed = match enrich::resize_cover_bytes(&picture_bytes) {
|
let processed = match enrich::resize_cover_bytes(&picture_bytes) {
|
||||||
Some(b) => b,
|
Some(b) => b,
|
||||||
None => {
|
None => {
|
||||||
@@ -1030,7 +1073,7 @@ pub fn run_mpd_cover_enrich(config: &MpdConfig, conn: &Connection) {
|
|||||||
continue;
|
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.
|
// and the online enrich command can find it.
|
||||||
let cover_path = dest.to_string_lossy().to_string();
|
let cover_path = dest.to_string_lossy().to_string();
|
||||||
match db::set_local_cover(conn, &album.artist, &album.album, &cover_path) {
|
match db::set_local_cover(conn, &album.artist, &album.album, &cover_path) {
|
||||||
|
|||||||
Reference in New Issue
Block a user