Correctness:
- MPD command reads no longer abandon a response half-way. The socket's
500 ms read timeout exists so the idle loop can poll the shutdown flag,
but it applied to every read, so a slow reply left unread bytes that the
next command parsed as its own. Add MpdConn::read_line/read_exact, which
retry across that timeout up to a 15 s deadline, and a desynced flag that
forces a reconnect when a read is genuinely abandoned. read_response now
returns Option so a truncated reply is a failure rather than an empty
result -- a timed-out currentsong used to look like "MPD is stopped".
- resolve_cover returned a covers/<name> path even when the file was
missing, so a deleted image produced a broken <img> instead of the
placeholder. Gate the return value on the same existence check as the
copy list.
- enrich --force no longer runs DELETE FROM album_cache. That discarded
pin-album entries, MPD-extracted cover paths, and any genre the re-fetch
then failed to find. Select every album via all_scrobbled_albums and
overwrite rows in place instead.
- Album grouping, cover fallback and source attribution disagreed: the
dominant_source subquery and artist_cover matched on album name alone, so
two records sharing a title cross-contaminated. Extract one
album_group_key_expr and use it everywhere.
- Escape quotes and backslashes in MusicBrainz Lucene queries; a title
containing a quote closed the term early and matched nothing.
- Size the HTML cover grid from the period's own limit, so all-time no
longer shows fewer covers than the table beneath it.
- Fix a panic in the terminal report: top_genres ranks broad single-word
genres last, so first() is not the busiest row, and a later genre with
more plays overflowed the bar width. Scale to the true maximum and clamp
the bar arithmetic.
- Bound readpicture transfers by size and chunk count so a bogus size:
header cannot drive an unbounded allocation or an endless loop.
Robustness:
- Kill the playerctl children before joining their reader threads. The
threads block until stdout closes, which only happened because an
interactive Ctrl+C signals the whole process group; under a supervisor
that signals one PID, shutdown hung and left orphans.
- Reject --limit and --all-time-limit below 1. SQLite reads a negative
LIMIT as unlimited, silently dumping the whole library.
- Set a 5 s busy_timeout, so running report against a live watch does not
fail immediately with "database is locked".
- Replace panics in default_db_path, covers_dir and the enrich queries with
reported errors, and check PRAGMA table_info for the source column
migration instead of discarding every ALTER TABLE error.
Cleanups:
- Collapse eight reset_*_timestamps functions into reset_retry_timestamps
with a RetryScope enum.
- Share one FNV-1a helper (enrich::cover_stem) between the MPD and iTunes
filename generators.
- Use params_from_iter to remove six duplicated query_map branches.
- Extract source_dot_cell and reuse db::split_genre_list in the report.
- Skip the genre lookup when only covers were requested, saving a request
and a rate-limit sleep per album.
- Keep the cached genre in pin-album when MusicBrainz returns nothing and a
--cover-url was supplied.