Commit Graph

12 Commits

Author SHA1 Message Date
andrew e07e931c45 0.6.12: Redesign the dedup review table for faster, more accurate scanning
The keep/delete columns were raw paths in a fixed-width cell: ellipsis-
truncated, full text only on hover. Slow to scan (mousing over every
row to read the song name) and, once "fixed" with a tag-derived title/
artist header in a first pass of this change, actively worse for the
thing dedup review actually needs -- confirming two files are really
the same recording. Tags can be wrong; the filename on disk can't.

New layout: each candidate gets a title row (song title, large,
unclipped -- parsed from beets' known singleton path template,
Artist/Album/Title.ext) followed by a compact detail row. The detail
row shows a color-coded format pill (FLAC/MP3/etc, matching the same
quality judgment dedup-library.sh's rank_file() already makes) and the
full relative file path, always rendered as visible text -- never
hover-only -- so a real difference in filename, album, or folder is
still plainly visible even when tags line up. Shared artist collapses
onto the title row so it isn't repeated per side; a colored left rail
marks which side survives without having to read the words.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 10:15:27 -06:00
andrew 2c86d7973f P2 minor set: password off argv, configurable share gate, POST logout, log path check
- Navidrome password is now passed to curl via stdin (--data-urlencode "p@-")
  in navidrome-scan.sh and pipeline-status.sh, so it no longer appears in
  ps/proc. Verified the query sent is identical and a live scan still triggers.
- MIN_ARTIST_DIRS (the share-health gate) is now a setting, threaded through to
  the pipeline env, so a user with a small library can lower it instead of the
  scan/sync being permanently blocked by the hardcoded 500.
- /auth/logout is now POST-only (with a nav form + aria-label), so a drive-by
  GET can't log the user out; enforced allowed_email already landed separately.
- view_log now confirms the run's log_path resolves under the logs dir before
  serving it (defense in depth).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:50:06 -06:00
andrew 9a116f3da4 Credential enable/disable + descriptions, artist casing settings page, playlist compare view
Credentials page now shows a fact-checked one-line description per
service and marks Spotify/Soulseek/Navidrome as Required (no toggle --
they're load-bearing for every playlist sync). The four optional
integrations (Bandcamp, AzuraCast, Qobuz, Telegram) get a real on/off
switch: Bandcamp pauses its scheduled job (same mechanism as the Jobs
page), the other three just remove their rendered credential file, which
the scripts that read them already treat as "not configured, skip" --
no script changes needed. Disabled optional scopes also drop off the
dashboard's credential status row.

New Settings > Artist Casing page to view/add/remove entries in
artist-canonical.list without shelling in -- adding a name that already
matches case-insensitively replaces its casing in place instead of
duplicating.

Playlist detail page's status section is now two side-by-side columns:
the original Spotify tracklist on the left, and a tabbed Downloaded /
Waiting-to-download view on the right (client-side tabs via Alpine),
both with an internal scroll area so long playlists don't pull the
columns out of sync. Uses the existing status_service reconciliation
(sldl index + beets + quarantine) -- no backend changes needed there.
2026-07-09 10:52:08 -06:00
andrew 76e5b32687 Fix library filter bar button misalignment
.field's margin-bottom (meant for vertically-stacked fields) pushed each
field's box lower than the bare Filter button under align-items: flex-end,
making the button look higher than the search/playlist/format boxes next
to it. New .filter-bar class zeroes that margin instead of the previous
ad-hoc inline flex styling.
2026-07-09 10:31:01 -06:00
andrew b4dd2286a9 Restructure Settings nav, modernize import page, dedup/genres UX polish
Settings is now one top-nav entry with Credentials and Jobs as sidebar
sub-pages (jobs moved from /jobs to /settings/jobs). Manual import page
replaces the bare file input and free-text filename field with a drag-drop
dropzone and a proper file-picker table. Genres page drops the "force"
checkbox for two explicit buttons (Preview / Fix genres now). Dedup's
"Scan now" runs both the file-naming and acoustic passes together, and the
table labels which pass caught each candidate instead of showing the raw
pass name. .btn-ghost gets a visible border so it reads as a real button
next to Delete/Danger actions instead of looking unaligned. Job names
throughout the UI are now human-readable instead of raw job_key strings.

Also includes the fpcalc exit-code fix from earlier (fingerprint index
was discarding valid fingerprints on files with a benign decode warning).
2026-07-09 10:26:40 -06:00
andrew 1f733d6e91 Add README and proper favicon/PWA icon set
README covers what alembic does, prerequisites, a full deploy quickstart,
credential setup, and a page-by-page usage tour, written for someone
running Docker but not necessarily comfortable coding or sysadmin work.

Favicon/icons were prepared in a separate theme scratch dir but never
wired in; moved them into app/static and pointed base.html at the full
icon set (SVG, PNG fallbacks, apple-touch-icon, manifest) instead of the
single inline data-URI SVG it had before.
2026-07-09 09:51:43 -06:00
andrew 92e5326437 Fix dedup apply lock-hang, mawk-broken tag passes; add track delete, fix button/select CSS
confirm_and_apply no longer marks candidates confirmed before their apply
job actually succeeds (a stuck/skipped_lock run was silently vanishing
confirmed deletes without deleting anything), switches the fuzzy pass to
--apply-pairs to avoid a full rescan on every confirm, and adds a job
timeout so a hung subprocess can't hold the global pipeline lock forever.

dedup-library.sh's Pass 2-4 use gawk-only features (gensub, POSIX interval
regex) but were running under mawk (Debian's default awk) in the deployed
image, throwing silent syntax errors on every run -- switched those
invocations to gawk explicitly and added it to the Dockerfile.

Also: per-track delete button on the library list/detail pages, and two
CSS fixes (the primary button's hover gradient was getting clobbered by
the base .btn:hover rule's plain background, and the select dropdown
arrow had no background-size so it rendered oversized).
2026-07-09 09:12:02 -06:00
andrew 491dd1844f Dedup page: shorten paths, fix column alignment, per-row delete
- Strip the common /data/music/Library/ prefix from displayed keep/delete
  paths (every track lives there, so it added width without adding
  information); full path is still available via a hover title.
- Table now uses a colgroup with fixed column widths so headers stay
  aligned with their content regardless of how long a given path is,
  instead of drifting with the widest cell in the column.
- Add a per-row Delete button (self-contained form per cell) alongside
  the existing checkbox + bulk "Delete selected" flow -- the checkboxes
  now target an external form via the HTML `form` attribute instead of
  wrapping the table in a form, since a form can't legally nest another
  form for the per-row buttons.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 16:31:21 -06:00
andrew 02744f6654 Fix library filter, compact dashboard/credentials UI, add fuzzy audio dedup
- beets_service: filter params now use truthy checks instead of `is not
  None`, since a real <select> left on "(any)" submits an empty string,
  not an absent param -- the filter form silently matched zero rows for
  any real submission. Also match grouping tokens within "; "-joined
  multi-playlist values instead of requiring an exact string match.
- dashboard: credential status renders as compact dot indicators instead
  of badge+text chips, so long scope names (telegram) stay on one line.
- credentials page: two-column grid layout instead of one long column.
- find-fuzzy-dupes.py: add --json/--only-paths flags matching
  dedup-library.sh's convention, so it can plug into the same review
  queue.
- dedup_review_service: add scan_fuzzy() and route confirm_and_apply()
  to the correct underlying script (dedup-library.sh vs
  find-fuzzy-dupes.py) per candidate's pass_name, since tag-based passes
  miss duplicates whose tags differ even when the audio is identical
  (e.g. a remix credited to different artists between two copies).
- dedup page: add a "Scan for audio duplicates" trigger alongside the
  existing tag-based scan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 16:25:52 -06:00
andrew ee21251f3e Apply new frontend theme
Full style rewrite (holographic/chrome dark theme) plus matching favicon,
authored externally and dropped in at /home/andrew/alembic-theme. Verified
every template's variable names, form field names, and action URLs match
the actual data each router passes before importing -- all matched exactly
with zero router/service changes needed. Validated all templates parse
(Jinja2 env.get_template over the full tree) and confirmed the live
container serves the new theme after rebuild+redeploy.
2026-07-08 15:01:39 -06:00
andrew 5601de3d44 Add playlists, credentials, and jobs pages; wire up dashboard
Vendored htmx.min.js (1.9.12) and alpine.min.js (3.14.1) into static/ --
self-hosted per the no-CDN/CSP requirement, included in base.html.

routers/playlists.py: list/add/remove playlists, a detail page showing
live per-playlist status via status_service (Spotify vs. beets vs. sldl
index vs. quarantine), edit form for active/cron_expr/no_m3u/notes, and a
"Run now" button wired to the jobs run-now endpoint.

routers/credentials.py: write-only credential forms per scope (a saved
secret is never sent back to the browser -- fields just show "(set)" and
leaving a field blank keeps its current value, matching
credential_service.set_credentials' partial-update semantics).

routers/jobs.py: maintenance job list with run-now/enable-disable, and a
recent-runs table that auto-refreshes via HTMX polling
(hx-trigger="every 5s") against a partial-only endpoint -- the one place
in the UI where avoiding a full-page reload actually matters, since job
status changes while the page is sitting open. Per-run log viewing.

Dashboard now shows real data (beets stats, playlist counts, recent job
runs) instead of the placeholder from Task 2.

Verified all 10 authenticated pages render successfully (200, no template
errors) via TestClient with require_auth overridden, including the
playlist detail page's graceful-degradation path when Spotify credentials
aren't configured yet (renders a "couldn't fetch live status" message
instead of a 500).
2026-07-08 14:24:46 -06:00
andrew bd56c8153f Add FastAPI app skeleton: settings, db, models, security, auth
- settings.py: pydantic-settings config for the two mount points
  (MUSIC_DATA_DIR, ALEMBIC_CONFIG_DIR), OIDC client config, encryption key path
- db.py: SQLite engine with WAL mode, plain-SQL schema migration runner
  (schema/*.sql, tracked by schema_version -- not the alembic migration tool)
- models.py: SQLAlchemy ORM models matching schema/0001_init.sql
- security/crypto.py: Fernet encrypt/decrypt for the secrets table
- security/oidc.py + routers/auth.py: Pocket ID OIDC login/callback/logout
- security/deps.py: require_auth dependency (redirect-to-login on no session)
- main.py: app factory, lifespan (init_db + beets WAL enable), session
  middleware, minimal dashboard route

Verified boots end-to-end via TestClient: unauthenticated GET / redirects to
/auth/login, static files serve, and all 13 schema tables get created on
first run.
2026-07-08 13:24:18 -06:00