Commit graph

6 commits

Author SHA1 Message Date
BoulderBadgeDad
eddaea2f93 watchlist history: record automatic scans too (#933)
save_watchlist_scan_run had a single caller — the manual scan endpoint. the automatic/
scheduled path (process_watchlist_scan_automatically) ran the full scan but never wrote a
history row, so nightly scans never showed up in the History modal — only manual ones.

- new shared helper persist_scan_run(database, state, ...) extracts the run from the
  finished watchlist_scan_state and writes one history row
- the automatic path now stamps scan_run_id/scan_track_events and calls it
- the manual path is refactored onto the same helper so the two can't drift apart again
- history is global (no profile filter), so the all-profiles nightly scan records one
  aggregate row (profile_id None → 1, never NULL)

tests: 4 new persist_scan_run seam tests (real DB) + 2 new auto-scan integration tests
proving the auto path actually records (completed + cancelled, exactly once). 420
watchlist/automation tests green.
2026-06-27 00:42:58 -07:00
Broque Thomas
f3ad65de34 Complete MusicBrainz watchlist source parity
Add MusicBrainz watchlist artist ID storage, badges, linked-provider editing, and per-artist preferred source support.

Backfill watchlist MusicBrainz matches from already-enriched library artists so existing MusicBrainz worker matches appear in watchlist cards and settings.

Extend bulk watchlist add, liked artist matching, artist map source picking, and service status labels to recognize MusicBrainz, with regression tests for watchlist ID persistence and backfill.
2026-05-18 19:19:25 -07:00
Broque Thomas
9602d1827c Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites
Catches the silent excepts the awk-based earlier sweeps missed:

- Bare `except:` followed by `pass` (also swallows KeyboardInterrupt
  and SystemExit — actively wrong). Upgraded to `except Exception as
  e: logger.debug("...: %s", e)`. ~14 sites across connection_detect,
  soulseek_client, listenbrainz_manager, watchlist_scanner,
  youtube_client, navidrome_client, jellyfin_client, web_server.
- `except Exception:` + pass that the awk pattern missed (e.g.
  multi-line or unusual whitespace). ~31 sites across automation_engine,
  database_update_worker, music_database, spotify_client, web_server,
  others.
- 14 legitimate cleanup sites left silent with explicit `# noqa: S110`
  + comment explaining why (atexit handlers, finally-block conn.close
  calls). Logging during shutdown can itself crash because file handles
  get torn down before the handler fires.

Also enables `S110` rule in pyproject.toml so this pattern fails CI
going forward — drift fails at PR review instead of at runtime against
a wedged worker thread. Tests path keeps S110 ignored (test fixtures
legitimately use try-except-pass for cleanup).

Adds a WHATS_NEW entry to helper.js summarizing the full #369 sweep.

Verified: `python -m ruff check .` → All checks passed.
Verified: `python -m pytest tests/` → 2188 passed.

Closes #369
2026-05-07 11:16:06 -07:00
Broque Thomas
8dc9f79f97 Surface silent exceptions in watchlist + discovery + reorganize — 18 sites
- watchlist_scanner.py: 6 sites
- discovery/playlist.py: 5 sites
- discovery/sync.py: 4 sites
- watchlist/auto_scan.py: 1 site (1 left silent — finally-block scanner cleanup)
- library_reorganize.py: 2 sites (4 left silent — all in finally blocks:
  conn.close, staging rmtree, sidecar delete, cleanup_empty_dir)

All non-finally sites converted to `logger.debug("...: %s", e)`.
Finally-block sites kept silent because logger calls during cleanup
(after exception was already raised) can themselves raise.

Refs #369
2026-05-07 09:52:20 -07:00
Broque Thomas
ef03901cb4 Bulk watchlist add: fall back through every source ID, not just active
The /api/library/watchlist-all-unwatched endpoint required the
user's currently active metadata source's ID column on each library
artist. A Spotify-primary user with library artists only matched
against iTunes or Deezer saw them silently skipped — surfacing on
Discord as "Library and Watchlist not syncing correctly". The per-
artist Enhanced View sync sometimes "fixed" them because it triggered
metadata enrichment that occasionally populated the missing Spotify
ID, but couldn't help artists Spotify simply doesn't carry.

Extracts the picker as a standalone helper so it can be tested
directly:

  core/watchlist/source_picker.py:pick_artist_id_for_watchlist

Picks the active source first when available, then falls back through
spotify -> itunes -> deezer -> discogs in registration order. Empty
strings count as missing. Numeric IDs are coerced to str so SQLite's
TEXT columns store them in the same form library code reads back.
Returns (None, None) only when the artist has zero source IDs — the
only legitimate skip reason now.

Adds 10 regression tests covering active-source priority for each
supported primary, fallback ordering through every secondary, the
zero-IDs base case, unrecognized active source (e.g. hydrabase still
falls through), empty-string handling, and numeric coercion.
2026-04-30 20:27:42 -07:00
Broque Thomas
2b2003ba4c Lift _process_watchlist_scan_automatically to core/watchlist/auto_scan.py
Pulls the 390-line watchlist auto-scan orchestrator out of `web_server.py`
into a new `core/watchlist/` package. Watchlist (followed-artists scanner
that finds new releases) is a separate domain from kettui's wishlist
(failed-download retry queue), so this lift does not overlap with the
ongoing PR400-style extractions.

What `process_watchlist_scan_automatically` does:

1. Smart stuck-detection guard before acquiring the timer lock —
   prevents deadlock when a previous scan flag is dangling past the
   2-hour timeout.
2. Inside the timer lock: re-check + set the active scan flag with the
   current timestamp.
3. Per-profile expansion (or single-profile when manually triggered):
   - Watchlist count check + Spotify auth gate.
   - Backfill missing artist images.
4. Initialize a fresh `watchlist_scan_state` dict (the deps property
   setter rebinds the web_server.py module-level name so external
   sentinel checks via id() comparison still detect the swap).
5. Pause enrichment workers, then call
   `WatchlistScanner.scan_watchlist_artists` with a per-event progress
   callback that translates scanner events into automation log lines.
6. Post-scan steps (skipped if the scan was cancelled mid-flight):
   - Populate discovery pool from similar artists (per-profile).
   - Refresh ListenBrainz playlists.
   - Update current seasonal playlist (weekly cadence).
   - Generate Last.fm radio playlists.
   - Sync Spotify library cache.
   - Activity feed entry + automation_engine.emit('watchlist_scan_completed').
7. On exception: mark state['status']='error', re-raise so the
   automation wrapper records the failure.
8. Finally: resume enrichment workers, clear the scanner's rescan
   cutoff, reset the auto-scanning flag.

Strict 1:1 byte parity:
The original mutated `watchlist_auto_scanning`,
`watchlist_auto_scanning_timestamp`, and `watchlist_scan_state` as
module globals (with a leading `global` decl). Here those names are
exposed through the `WatchlistAutoScanDeps` proxy as Python properties
so the lifted body keeps the same `name = value` / `name[key] = value`
shape. Property setters fan writes back to web_server.py via callback
pairs.

Diff vs original after `deps.X` → global X normalization is **zero
differences** apart from the dropped `global` declaration line — Python
doesn't need it once the names are property accesses on the deps object.
390 lines orig = 390 lines lifted, byte-identical body otherwise.

Dependencies injected via `WatchlistAutoScanDeps` (15 fields total) —
Flask app, spotify_client, automation_engine, watchlist_timer_lock, plus
5 callable helpers and 6 property delegate callbacks (paired
get/set for each of the three globals).

Tests: 11 new under tests/watchlist/test_auto_scan.py covering
stuck-detection guard, race-check inside lock, zero-watchlist short-
circuit, unauthenticated Spotify gate, successful scan with all post-
scan steps, automation event emission, activity feed logging,
cancellation mid-scan skipping post-steps, profile-scoped trigger,
flag reset in finally, rescan cutoff clear in finally.

Full suite: 1319 passing (was 1308). Ruff clean.
2026-04-29 08:17:41 -07:00