Commit graph

970 commits

Author SHA1 Message Date
Broque Thomas
ddbcdfe73a MusicBrainz: Filter live/compilation bootlegs + chronological sort
Three related fixes to make album/track results look like a real
artist discography instead of a firehose of fan-compiled bootlegs.

1. Drop 'compilation' from the release-group browse primary-type filter.
   MB's OR filter (`type=album|ep|single|compilation`) silently breaks
   when 'compilation' is included — Metallica drops from 1076 matches
   to 82 because `compilation` is a SECONDARY type on MB, not a primary
   type. The invalid value corrupts the filter for all types, not just
   itself. Now we request `type=album|ep|single` which returns the full
   1076; actual compilations (primary=Album + secondary=[Compilation])
   are filtered out by the studio-preference logic below.

2. Filter release-groups with non-studio secondary-types
   (Live/Compilation/Soundtrack/Remix/Demo/Mixtape/Interview/Audiobook/
   Audio drama). For Metallica, the first 100 browse results are 12
   studio albums + 83 live bootlegs + 5 compilations — without this
   filter the Albums section was dominated by 2019-2021 broadcast
   recordings. Falls back to the unfiltered list if filtering leaves
   the result set empty (covers live-only niche artists).

3. Sort chronologically ASC by first-release-date. Wikipedia-style
   discography ordering — debut album on top, then chronological.
   Previous DESC sort put the most recent release on top which, for
   prolific artists, meant 2020s material before their classics.

Track side of the same fix:

- Re-orders each recording's `releases` array to put studio releases
  first before `_recording_to_track` picks up the first release for
  album context. Without this, MB's arbitrary release order often
  buried the canonical studio album under random live bootlegs.
- Filters out recordings that only exist on live/compilation release-
  groups (keeps the ones with at least one studio release). Falls
  back to the full set if the artist has no studio recordings at all.
- Sorts recordings by earliest studio-release year ASC so classic
  tracks surface first.

Smoke test against live MB API confirmed:
- Artists: [Metallica score=100]
- Albums: Kill 'Em All (1983) → Ride the Lightning → Master of Puppets
  → ...And Justice for All → Metallica (Black Album) → Load → Reload
  → St. Anger → Death Magnetic → Lulu (2011)
- Tracks: real Metallica recordings (Killing Time, Nothing Else
  Matters, Creeping Death, etc.) — a few remastered demos still leak
  in where MB metadata quality is thin, but the bulk is correct.
- Total latency: 3.5 seconds.

4 new tests covering the studio filter, live-only fallback, preferred
release ordering, and live-only recording exclusion.

Credit: kettui flagged the poor MB results during PR #371 review.
2026-04-24 08:32:05 -07:00
Broque Thomas
8523724b03 MusicBrainz: Switch track lookup from browse to arid: search
The previous commit's `browse_artist_recordings` call passed
`inc=releases+artist-credits` — but MusicBrainz's recording browse
endpoint rejects `inc=releases` with HTTP 400. The adapter's error
handler returned an empty list, so the Tracks section stayed empty
even though the fix was supposed to populate it.

Browse without release info is useless for our search UI (tracks
would render with no album), so swap to the fielded Lucene search
`arid:<mbid>` on the `/recording` endpoint. That's the canonical MB
pattern for "find recordings by this artist WITH release context":
- arid: search accepts the artist MBID and returns recordings with
  `releases` (release-group, date, media) embedded in each result.
- One API call per lookup, same as browse would have been.

Renamed the method to `search_recordings_by_artist_mbid` so the name
matches its behaviour — it's a search, not a browse. Adapter updated
to call the new name; tests updated to match.

Verified against the live API: Metallica's MBID returns 5 recordings
in ~1.8 seconds (vs the previous 400 error).
2026-04-24 08:25:09 -07:00
Broque Thomas
394ac73877 MusicBrainz: Tests for new search behavior + WHATS_NEW entry
26 new unit tests in tests/test_musicbrainz_search.py covering:

- Cover Art URL construction (release + release-group scope, empty MBID,
  unknown scope fallback)
- Structured query splitting (hyphen, en-dash, em-dash, bare name, no
  false-positive splits on hyphens-inside-words)
- Artist search: score filtering, strict=False call contract, exception
  handling, genre extraction from MB tags, mbid/name validation
- Top-artist resolver: memoization by normalized query, sub-threshold
  returns None, negative-result caching, empty-query short-circuit
- Album search routing: bare query → browse path, structured query →
  text path, no-artist-match falls back to text, text path score filter
- Track search routing: browse path, dedupe-by-title across
  live/compilation variants, structured query → text path, text path
  score filter

All mock the underlying MusicBrainzClient — no network calls.

Also adds a WHATS_NEW entry under 2.40 explaining the three user-visible
changes: Artists section now populates, album/track results match the
searched artist instead of random title collisions, and search completes
in ~3 seconds instead of 30+.
2026-04-24 08:18:47 -07:00
Broque Thomas
14893c85a9 Extract _build_source_only_artist_detail into core/artist_source_detail.py
JohnBaumb's review: "If we're going to refactor the web_server.py soon,
might as well start moving stuff away from web_server.py in our PRs.
_build_source_only_artist_detail, make it a module, it's perfect."

This continues the pattern the prior commit started with the source-ID
lookup helpers: move the pure data-building logic to a side-effect-free
core module, leave a thin wrapper in web_server.py that bridges the
Flask response and the module-global clients.

**core/artist_source_detail.py** — pure function that takes the artist id,
name, and source plus dependency-injected per-source clients (spotify,
deezer, itunes, discogs) and a Last.fm API key. Returns
(payload_dict, http_status) so it isn't coupled to Flask.

**web_server.py wrapper** — builds the client bag from the module globals
(checks Spotify auth, constructs the Discogs client from the configured
token, reads the Last.fm API key) and wraps the core return in jsonify.
147 lines of logic go away from web_server.py; the 24-line wrapper is
purely glue.

**tests/test_artist_source_detail.py** — 21 focused tests covering the
response envelope, the source-specific ID-field stamping for all six
supported sources, the dedup_variants=False contract (the behaviour
that originally motivated the split of MetadataLookupOptions), per-source
genre/follower extraction with safe handling of missing or throwing
clients, and the Last.fm enrichment branch including the no-key and
error-path cases. Runtime 0.26s.
2026-04-23 08:09:29 -07:00
Broque Thomas
e66af77ff6 Make artist_name Optional in find_library_artist_for_source
Cin's review note: typing artist_name as plain `str` forced callers
that didn't have a name to pass `""` as a placeholder, which leaks the
parameter's emptiness contract into every call site and reads badly in
tests. Switching to `Optional[str] = None` lets callers omit it.

The function body's `if artist_name and active_server:` check already
handles None and "" identically, so no body changes were needed. Tests
that previously passed `artist_name=""` drop the argument; one new test
covers the omitted-arg path explicitly.

The web_server.py wrapper takes the same default for symmetry.
2026-04-22 22:15:03 -07:00
Broque Thomas
a097cf3d5a Extract source-artist lookup helpers from web_server.py to core module
Cin pointed out that the prior version of test_artist_source_lookup.py
AST-parsed web_server.py to verify a constant and to string-match a
function's response keys. That was a workaround for the fact that
web_server.py can't be imported at test time (it boots Spotify,
Soulseek, Plex, etc.) — the right answer is to move the logic into a
side-effect-free module so it can be imported and tested directly.

This commit:
  - adds core/artist_source_lookup.py containing the SOURCE_ID_FIELD
    map, the SOURCE_ONLY_ARTIST_SOURCES set, and find_library_artist_for_source
  - replaces the inline definitions in web_server.py with imports +
    a thin wrapper that injects the active media server
  - rewrites the tests to import from the core module directly:
      * mapping correctness is now a plain equality assertion
      * lookup behaviour is exercised against a real MusicDatabase
      * the AST parse and the string-matching contract test class are
        gone
  - drops the _build_source_only_artist_detail contract test entirely
    (the weakest of the four — it was just string-matching the function
    body); when that function moves to core/ it can get a real
    behavioural test alongside.

Test runtime drops from ~161s to ~5.8s. All 18 tests pass.
2026-04-22 22:07:23 -07:00
Broque Thomas
12c23b6b89 Add regression tests for source-artist lookup + dedup_variants flag
Four targeted backend tests for behaviour added during the Search/Artists
unification work:

1. _SOURCE_ID_FIELD mapping is parsed out of web_server.py via AST and
   compared against an explicit expectation, so silent renames break the
   test instead of silently breaking library-upgrade detection.

2. Every column in _SOURCE_ID_FIELD must exist on the real artists table
   after migrations run. This is the schema-vs-query contract that the
   `deezer_artist_id` typo would have failed instantly.

3. The two queries from the watchlist-config enrichment path execute
   verbatim against a fresh DB — separate ones for the artists table
   (deezer_id / discogs_id) and the watchlist_artists join (deezer_artist_id).
   Documents the column-name split that caused the original bug.

4. Static contract test for _build_source_only_artist_detail's response
   shape: every JSON key the frontend reads (success/artist/discography/
   image_url/server_source/genres/lastfm_*) must appear in the function
   source, plus the dynamic source-id stamp and the dedup_variants=False
   opt-out.

Plus a behavioural test for MetadataLookupOptions.dedup_variants=False
in test_metadata_service_discography.py — proves the flag actually keeps
variant releases that get_artist_detail_discography would otherwise
collapse to a single canonical entry.
2026-04-22 21:14:11 -07:00
Broque Thomas
7625362c49 Fix date-dependent watchlist scanner tests failing on CI
The three discovery-pool tests hardcoded release_date strings
("2026-04-01", "2026-04-16") that were checked against a rolling
`datetime.now() - timedelta(days=7)` (or 21-60 day) cutoff in the
scanner. Once the wall clock advanced past the cutoff window the
releases were filtered out and the assertions failed — Python 3.11
Linux CI was already past 2026-04-23 UTC.

Replace the hardcoded values with a module-level
`_RECENT_RELEASE_DATE = now - 2 days` so the fixtures stay inside
every cutoff window regardless of when the suite runs.
2026-04-22 20:56:10 -07:00
Broque Thomas
93f1941829 Unify artist detail: route source artists to standalone page, retire inline Artists page
Completes the artist-detail unification. Source artists now land on
the same /artist-detail page as library artists (with the source-aware
backend endpoint from earlier this session handling the data fetch).
The inline Artists page is gone — artists.js deleted, #artists-page
HTML block removed, /artists URL aliases to /search.

  Source-artist callsites re-migrated from selectArtistForDetail to
  navigateToArtistDetail (search results, global widget, download
  modal, Discover hero / Your Artists cards / artmap context / genre
  deep-dive, watchlist artist detail).

  Visual upgrade to standalone hero: added .artist-detail-hero-bg +
  .artist-detail-hero-overlay (blurred image bg, dark gradient — same
  treatment as the inline page). library.js sets the bg image when
  loading an artist.

  Library-only UI hidden via CSS for source artists (existing rules
  from the previous commit cover Enhanced toggle, Status filter,
  completion bars, enrichment coverage, Top Tracks sidebar, Radio /
  Enhance buttons).

  Final 2 helpers (lazyLoadArtistImages used by wishlist-tools,
  showCompletionError used by completion checker) moved from
  artists.js into shared-helpers.js. The inline-page candidate set
  was dropped from _resolveSimilarArtistsTargets.

  init.js: 'artists' alias added at top of navigateToPage (same
  pattern as the existing 'downloads' alias). 'case artists:' handler
  removed from loadPageData. _getPageFromPath now maps artist-detail
  to library as its parent (matches the existing nav highlight at
  init.js:2161).

  tests/test_script_split_integrity.py: artists.js removed from
  SPLIT_MODULES; KNOWN_CROSS_FILE_DUPES updated to point escapeHtml
  at shared-helpers.js instead of artists.js. 354/354 tests pass.

  Net delta: -1700 lines.

Stays at 2.39. Once you've verified end-to-end (library artist ->
hero looks like inline visual; source artist from Search -> same
page, similar artists works, no 404s; /artists URL -> /search), a
follow-up commit bumps to 2.40 with the full WHATS_NEW entry that's
already prepped.
2026-04-22 17:00:52 -07:00
Broque Thomas
1a8071d6ec Revert "Retire artists.js and inline Artists page, ship unification at 2.40"
This reverts commit 71ff5cb5c3.
2026-04-22 15:52:53 -07:00
Broque Thomas
71ff5cb5c3 Retire artists.js and inline Artists page, ship unification at 2.40
Part D + E of the deferred cleanup + the final version bump that
publishes the whole Search/Artists unification project.

Deletions:
  - webui/static/artists.js (1903 lines) — removed entirely. The 2
    remaining externally-referenced helpers (lazyLoadArtistImages +
    showCompletionError) moved into shared-helpers.js first.
  - webui/index.html — 140-line #artists-page HTML block and the
    <script src="artists.js"> tag both removed.

init.js wiring:
  - 'case artists:' removed from loadPageData switch (no page to init).
  - navigateToPage top-level alias extended: 'artists' → 'search'
    (same pattern as the existing 'downloads' → 'search' alias).
    Legacy /artists bookmarks land on the unified Search page, the
    natural place to find an artist now.
  - _getPageFromPath now maps artist-detail → library as its parent
    (was artists). Matches the existing library-nav-highlight at
    init.js:2161.

Version bump:
  - _SOULSYNC_BASE_VERSION 2.39 → 2.40.
  - WHATS_NEW entries lose the 'unreleased' scaffolding and gain a
    new top entry summarizing the unified artist-detail page + the
    final artists.js retirement.
  - version-info modal gets a 'Search & Artists Unification' section
    at the top.
  - The _getLatestWhatsNewVersion filter added during the unreleased-
    tracking phase is rolled back — entries now display as soon as
    they land in WHATS_NEW, matching the pre-unification behaviour.

Test suite:
  - tests/test_script_split_integrity.py SPLIT_MODULES updated:
    'artists.js' dropped, 'shared-helpers.js' added. escapeHtml's
    cross-file dupe list entry updated to reference shared-helpers.
  - 354/354 tests pass.

User-visible result after this commit:
  - Sidebar: Search, Downloads, Discover, Library, Wishlist, etc. —
    no more Artists entry.
  - Click any artist anywhere: lands on the same /artist-detail page.
  - Search page has a source dropdown; Soulseek is just another option.
  - Legacy /downloads and /artists URLs alias to /search.
  - Version button shows v2.3 (Docker major); "What's New" panel
    opens to the unification summary.

Closes the project Cin requested in Discord. Future work: source-aware
/api/artist-detail could be extended to fall back through the whole
source priority chain when a specific source is given but returns no
discography. Not needed for the current flows.
2026-04-22 15:38:32 -07:00
Broque Thomas
a5d97261e4 Extract shared helpers from artists.js to shared-helpers.js
Part C of the deferred unification cleanup. The Artists page is no
longer in the sidebar, but its JS file can't be deleted yet because
it houses ~20 general-purpose helpers that other modules depend on
(escapeHtml used in 229 places, service-status polling, image-colour
extraction, download-bubble infrastructure, discography completion
checking, enrichment card rendering).

Moved all non-page-specific code from artists.js into the new
webui/static/shared-helpers.js — pure copy/paste, zero logic change.
Two contiguous blocks extracted:

  Block A (lines 1097..1398 of original artists.js): discography
    completion suite — checkDiscographyCompletion, handleStreaming-
    CompletionUpdate, cacheCompletionData, updateAlbumCompletion-
    Overlay, getCompletionStatusText, setAlbumDownloadedStatus,
    setAlbumDownloadingStatus.

  Block B (lines 2206..EOF of original artists.js): download-bubble
    infrastructure (artist + search + Beatport clusters with their
    snapshot/hydrate/modal/monitor helpers), openDownloadMissingModal-
    ForArtistAlbum, image-colour extractor and dynamic-glow helper,
    escapeHtml, service-status polling, renderEnrichmentCards.

Function declarations in a plain <script> tag are auto-global, so all
existing callers continue to resolve without any import/export
changes. Load order in index.html: shared-helpers.js loads right
after core.js (which defines the artistDownloadBubbles / search-
DownloadBubbles / beatportDownloadBubbles globals these helpers use).

Stats:
  artists.js:       4638 → 1903 lines (-2735)
  shared-helpers.js: new, 2762 lines
  No function duplicated between the two files
  All 357 tests pass (3 new from split-integrity parametrization)

What's left in artists.js is purely the Artists page — search UI,
detail view, state switching, watchlist button, discography loading.
All of that is reachable only by typing /artists in the URL bar
since the sidebar entry was retired in Phase 4b. Parts D + E will
delete that remainder and the file itself.
2026-04-22 15:25:59 -07:00
Broque Thomas
6992e2e5b5 Rename Search page id from 'downloads' to 'search', bump to 2.43
Phase 3b of the Search/Artists unification. The Search page's
internal id was 'downloads', which clashed with the actual Downloads
page (id 'active-downloads') and confused anyone reading the code.
Renamed to 'search' across HTML, navigation, DOM selectors, and the
deep-link route list.

Backwards compat: navigateToPage('downloads') aliases to 'search'
at the top of the function; /downloads URL still serves index.html
and the client router resolves the page correctly; profile ACL
checks accept both 'search' and 'downloads' so existing profiles
with 'downloads' in allowed_pages keep working without migration.

Sidebar label unchanged. Zero visual change — pure internal tidy.
2026-04-22 13:22:49 -07:00
Broque Thomas
d055e53610 Repoint websocket transport test to core.js after split
The websocket init block moved from script.js into core.js in the
module split (PR #352). Test was still hardcoded to the old path.
2026-04-22 11:30:39 -07:00
BoulderBadgeDad
00f116ebee
Merge pull request #352 from JohnBaumb/refactor/split-script-js
Split monolithic script.js into 17 domain modules
2026-04-22 10:49:17 -07:00
Broque Thomas
0d0bbf38c9 Add query-shortening retry + qualifier guard to Tidal search
Tidal's search engine chokes on long queries with multiple qualifier
words (remix credits, edit labels, bonus-disc markers). User reported
case: "maduk transformations remixed fire away fred v remix" returns 0,
but shortening to "maduk transformations remixed fire away" works.

Behaviour change:
- On a 0-result search, retry with progressively-shortened variants
  (capped at 5 total attempts, 100ms pause between).
- Variants (in priority order):
    1. strip trailing "(...)" / "[...]"
    2. strip all parentheticals/brackets
    3-5. drop last 1 / 2 / 3 tokens
    6. keep first half of tokens (rounded up)
- Dedupes so identical variants don't re-query.

Safety — qualifier-aware filter:
- Variant keywords (Live / Remix / Acoustic / Extended / Unplugged /
  Instrumental / Karaoke / etc.) are extracted from the original query
  using word-boundary match so "edit" doesn't match "edition" and
  "mix" doesn't match "remixed".
- If the original query carries any qualifiers, fallback results MUST
  contain those qualifiers in their track names — otherwise a shortened
  query could silently downgrade "Song (Live)" to the studio "Song".
- Tracks that fail the filter are dropped. If no variant produces
  qualifier-matching tracks, returns ([], []) — the same outcome as the
  original code, so no regression.

Contract preservation:
- Never raises to caller (outer try/except catches orchestration errors).
- Returns ([], []) on any failure path, same as original.
- Original-query successes take the same code path as before — no
  behavioural change for queries that already work.
- Defensive guards for None/empty/non-string query (early return).

Logging:
- Preserves original warning/error/info messages for back-compat log
  scraping.
- Adds fallback-success INFO log ("Tidal fallback query succeeded: ...")
  so successful retries are visible in production logs.
- Adds qualifier-filter INFO/DEBUG logs with kept/total counts.
- Per-attempt exception logs at DEBUG (not ERROR) to avoid noise when
  retries succeed.
- Traceback preserved on final failure.

Tests (16 regression tests in tests/test_tidal_search_shortening.py):
- Skowl's reported query reaches his working variant within the cap.
- Paren/bracket stripping priority.
- Short queries produce no variants.
- All variants unique (dedup guard).
- Progressive token drops present for long queries.
- Qualifier extraction is word-bounded (no "edit" in "edition").
- Qualifier extraction is case-insensitive.
- Track name filter requires ALL qualifiers.
- Empty-qualifier list passes every track (original-query behaviour).

All 292 tests pass.
2026-04-22 07:42:16 -07:00
JohnBaumb
77b069acf4 Add split integrity tests (61 tests) 2026-04-22 00:04:16 -07:00
Broque Thomas
e5d4d61c0e Fix watchlist content filters: live false positives + auto-scan bypass
Two bugs reported in issue #320:

1. Auto-watchlist scan bypassed Global Override settings.
   scan_watchlist_profile applied _apply_global_watchlist_overrides, but
   the scheduled auto-scan called scan_watchlist_artists directly —
   bypassing the override. Users who unchecked "Albums" or "Live" under
   Watchlist → Global Override still saw full albums and live tracks
   added during nightly scans (per-artist defaults, which include
   everything, won).

   Moved override application into scan_watchlist_artists itself so
   every entry point respects it. scan_watchlist_profile now forwards
   the apply_global_overrides flag through to avoid double-application.

2. is_live_version (watchlist + discography backfill) and
   live_commentary_cleaner's content patterns used bare \blive\b, which
   matched verb uses like "What We Live For" by American Authors,
   "Live Forever" by Oasis, "Live and Let Die" by Wings.

   Tightened the live patterns to require clear recording context:
   (Live) / [Live Version] / - Live / Live at|from|in|on|version|
   session|recording|performance|album|show|tour|concert|edit|cut|take
   / In Concert / On Stage / Unplugged / Concert.

   Locked in 11 regression tests covering the reported false positives
   (What We Live For, Live Forever, Living on a Prayer, Live and Let Die)
   and the reported true positives (Dimension - Live at Big Day Out,
   MTV Unplugged, etc.).

Version bumped to 2.37 with changelog entries.
2026-04-21 19:16:25 -07:00
Broque Thomas
6314827e91 Fix test-order-dependent failures in new metadata_service tests
The _DummyConfigManager stubs in test_metadata_service_musicmap.py and
test_metadata_service_artist_image.py were missing get_active_media_server(),
which the existing test_metadata_service_discography.py dummy provides.

Both files install their dummy via sys.modules["config.settings"] with an
"if not in sys.modules" guard, so whichever test file loads first wins.
When the new files load alphabetically before discography, the limited
dummy persists and later tests hit AttributeError on get_active_media_server.

Adds the same get_active_media_server method to both dummies so all three
test files are equivalent and test ordering no longer affects outcomes.
2026-04-21 11:39:56 -07:00
Antti Kettunen
6f79214439
Route artist image lookup through metadata service
- Move /api/artist/<artist_id>/image resolution into core.metadata_service.
- Resolve artist artwork through source priority, with explicit source/plugin overrides preserved.
- Keep Spotify call tracking inside the client layer to avoid double counting.
- Update similar-artist lazy loading to pass source context and add service coverage.
2026-04-21 21:14:35 +03:00
Antti Kettunen
b022a90997
Move MusicMap similar artist matching into metadata service
- Relocate the streamed MusicMap similar-artist flow out of web_server.py and into core.metadata_service.
- Match similar artists through the configured source-priority chain instead of assuming Spotify first.
- Add iTunes artwork fallback so streamed artist payloads still carry image_url when search results are sparse.
- Cover the new service behavior with tests.
2026-04-21 21:14:35 +03:00
Broque Thomas
7287a9d184 Fix test using old deezer_track_id column name
The unknown_artist_fixer was updated to use deezer_id (matching the
actual tracks table column) but the test still passed deezer_track_id
in the track dict, causing the deezer lookup to miss and fall back
to Spotify.
2026-04-20 21:11:45 -07:00
Antti Kettunen
24abae6908
Refactor album track lookup to use source priority
- Move album-track resolution into metadata_service
- Use the configured provider order instead of Spotify-first branching
- Switch the frontend to the unified /api/album/<id>/tracks endpoint
- Add tests for source-priority lookup, DB resolution, and formatting
2026-04-20 21:29:46 +03:00
Antti Kettunen
bd3b080025
Fix artist-detail source id lookup
- pass provider-specific artist ids into the source-priority discography lookup
- stop relying on the local library artist id when querying external metadata
- add a regression test for source-specific artist id resolution
2026-04-20 15:12:14 +03:00
Antti Kettunen
9fc757ce49
Fix library artist details failing to fetch correct album information
- Stop passing in spotify_id as the id in the UI, use the actual db id instead
  - Fixes an issue where albums for another artist would end up being returned for the actual searched artist
- Remove the redundant artist_id filtering code
  - Fixes an issue where not-currently-owned albums would be filtered out from the results, even if they were successfully fetched from the configured metadata provider
2026-04-20 08:14:28 +03:00
JohnBaumb
d6258824fb test: listening stats worker batched query paths
Coverage for fix 2.1:

TestResolveDbTrackIdsBatch:

  - Batch returns the same (title, artist) -> id mapping as the

    per-event lookup would have

  - Case-insensitive matching preserved

  - Empty event list returns an empty dict

  - Events without a title are skipped

  - A cursor-execute counter proxy confirms 50 events trigger exactly

    one SQL query (not 50)

TestMapPlayCountsToDb:

  - Returns updates only for server IDs that exist in tracks

  - Empty input returns an empty list

  - 30 server IDs trigger one batched query

TestEnrichStatsItems:

  - Populates image_url / id / artist_id on matching artists, albums,

    and tracks; skips rows with no match

  - Empty or missing top_* lists are safe

  - Three batched queries total (one per section) regardless of the

    number of items in each list
2026-04-19 15:22:25 -07:00
JohnBaumb
5a126f6562 test: api/request periodic cleanup timer
Coverage for fix 4.2:

  - _cleanup_old_requests evicts entries older than _MAX_REQUEST_AGE

    and leaves fresh entries intact; returns the number removed

  - Empty map is safe (no error)

  - start_cleanup_thread is idempotent (returns False on second call)

  - stop_cleanup_thread joins the thread and clears the handle

  - The thread actually evicts stale entries on wakeup

  - stop signals the thread to exit promptly via the stop event

    instead of waiting for the next interval
2026-04-19 15:22:25 -07:00
JohnBaumb
af8a2ea31a test: downloads endpoint pagination and filtering
Covers fix 4.1:

  - Default limit (100) applied when no params given

  - limit and offset slice correctly without overlap between pages

  - status param accepts single or comma-separated values

  - Unknown status returns empty list with total=0

  - limit is clamped to a max of 500

  - Negative or non-integer limit/offset fall back to safe defaults

  - Tasks are returned newest-first by status_change_time
2026-04-19 15:22:25 -07:00
JohnBaumb
8c827f6d3b test: enrichment worker re-processing fix and migration backfill
Coverage for fix 1.1:

TestBackfillMigration verifies the one-shot migration sets

match_status='matched' for rows that already have a populated

external ID (lastfm_url, musicbrainz_release_id,

musicbrainz_recording_id, tidal_id, qobuz_id) but NULL match_status,

and leaves rows without an ID untouched.

TestGetExistingIdColumnMapping verifies lastfm_worker reads

lastfm_url for all entity types and musicbrainz_worker reads the

correct per-type column (musicbrainz_id / musicbrainz_release_id /

musicbrainz_recording_id).

TestLastFMWorkerMarksMatched / TestTidalWorkerMarksMatched /

TestQobuzWorkerMarksMatched / TestMusicBrainzWorkerMarksMatched

verify each worker's _process_* short-circuit path sets

match_status='matched' (and does not re-call the external API) when

the entity already has an ID populated.
2026-04-19 15:22:24 -07:00
JohnBaumb
b1c2e89595 test: api_search_tracks single-query track search
Covers dict-row return shape, full-column presence (incl. file_path),
empty query handling, no-match results, artist-only match, limit
enforcement, fuzzy fallback, and backward compatibility of
search_tracks (DatabaseTrack return shape preserved for existing
internal callers).
2026-04-19 15:22:24 -07:00
JohnBaumb
2f19af779b test: wishlist SQL pagination and category filter
Covers LIMIT/OFFSET paging, album/singles category filtering (including
EP/compilation/missing album_type edge cases), COUNT with and without
category, profile isolation, backward-compat no-args behavior for
existing callers, and date_added ordering.
2026-04-19 15:22:24 -07:00
JohnBaumb
133af45199 test: metadata cache batch entity lookups
Covers original ordering preservation, partial/full hit thresholds,
empty result_ids, TTL expiration, cache miss behavior, and a
round-trip count assertion confirming 50 entities resolve in a
single SELECT (not 50).
2026-04-19 15:22:24 -07:00
JohnBaumb
4cfe6c6bf8 test: auth last_used_at write throttle
Covers first-write persistence, in-window suppression, post-window
rewrite, per-key independence, thread safety under concurrent access,
and the documented 15-minute interval.
2026-04-19 15:22:24 -07:00
BoulderBadgeDad
e0f036df08
Merge pull request #328 from JohnBaumb/feature/deep-linking
Add URL-based deep linking for SPA navigation
2026-04-19 13:07:46 -07:00
Broque Thomas
c940363ec2 Fix CI test failures from incomplete dummy config and encoding
8 test files had _DummyConfigManager missing get_active_media_server(),
causing failures when pytest ran them before the test file that had it.
Whichever file set sys.modules first won, and the incomplete dummy broke
later tests. Also fix script.js read_text() missing encoding='utf-8'
which failed on non-UTF-8 default locales.
2026-04-19 13:04:55 -07:00
JohnBaumb
5af4dc7853 test: add unit tests for SPA deep-linking catch-all route 2026-04-19 10:56:13 -07:00
Antti Kettunen
32e2281b9c
Refine variant release dedup
- broaden the artist-detail dedup helper to catch trailing parenthetical edition and remaster variants
- keep the legacy hyphenated suffix fallback for older metadata
- add regression coverage for language-specific Edition and remaster cases
2026-04-19 20:38:50 +03:00
Antti Kettunen
33b4ea6429
Refine artist-detail discography flow
- move artist-detail discography resolution onto the shared source-priority metadata service
- keep the variant dedup helper in the UI-facing adapter
- pass the chosen source through completion checks
- add coverage for the new adapter and dedup behavior
2026-04-19 20:38:49 +03:00
Antti Kettunen
17865fe712
Refactor artist discography completion metadata flow
Move completion checks into metadata_service and make them follow the configured metadata source priority.

Drop the old test-mode path, remove the web_server wrapper indirection, and keep artist inference on explicit release metadata instead of guessing from a track search.

Add coverage for the source-priority completion behavior and the safer artist-name handling.
2026-04-19 15:40:08 +03:00
Antti Kettunen
cca396e7bd Centralize Hydrabase enablement
Move Hydrabase availability checks into metadata_service so source resolution owns the policy. Keep web_server delegating to the centralized helper and add tests for the enabled/disabled cases.
2026-04-18 18:47:54 +03:00
Antti Kettunen
2b575a59ae Refactor artist discography lookup
Move artist discography resolution into core metadata_service, introduce MetadataLookupOptions, and keep web_server focused on request handling. Add focused tests for the new service boundary and preserve current fallback behavior for now.
2026-04-18 18:41:31 +03:00
BoulderBadgeDad
6b70d7331c
Merge pull request #295 from pxjx22/fix/socketio-polling-fallback
Fix Socket.IO client fallback order
2026-04-17 13:12:22 -07:00
BoulderBadgeDad
0cf5cbe9cd
Merge pull request #311 from kettui/feat/remove-desktop-app
Remove desktop app, clean up test files, remove unused dependencies
2026-04-17 12:52:10 -07:00
Antti Kettunen
0e2aa42b10 Realign test file names 2026-04-17 21:25:01 +03:00
Antti Kettunen
279000435b Trim redundant websocket phase tests
Keep room/subscription, parity, and state-reflection coverage.
Drop bare receipt checks and duplicated HTTP smoke tests.
2026-04-17 21:20:15 +03:00
Antti Kettunen
0e85931cc8 Fix track ID generation in repair flows
Repair-worker album fills now generate explicit track IDs when copying rows, instead of relying on SQLite auto-assignment that no longer exists for TEXT primary keys. The unknown-artist fixer now does the same for new artists.

Also add a regression test for the album-fill copy branch and keep the AcoustID scanner resilient to legacy null-ID rows.
2026-04-17 19:26:08 +03:00
Antti Kettunen
88e2527b96 Fix null-pointer error in acoustid_scanner
The root cause (null track ids) needs to be solved elsewhere, but this is a band-aid for now
2026-04-17 19:17:24 +03:00
Antti Kettunen
eead0c3dac Clarify similar-artist freshness and backfill
Freshness is now age-only, and scan-time backfill runs separately without Spotify-auth gating or retired iTunes compatibility flags.
2026-04-17 09:53:03 +03:00
Antti Kettunen
8382b8e247 Refactor similar artist backfill
Switch similar-artist backfill to the shared provider-priority flow instead of assuming iTunes as the fallback.
Reuse the generic metadata search helpers, keep a compatibility alias for the old helper name, and update the scanner tests to cover the new path.

Add a regression test that verifies backfill walks each available fallback provider and persists the resolved IDs per source.
2026-04-17 09:33:01 +03:00
Antti Kettunen
47a6c257ad Refactor MusicMap similar artist matching
Shift similar-artist lookup to the shared metadata provider priority flow.
Use generic provider clients for search and metadata extraction instead of
branching on Spotify/iTunes-specific paths.

Add a regression test that verifies MusicMap matching queries the provider
priority list and preserves canonical metadata from the best match.
2026-04-17 09:24:05 +03:00
Antti Kettunen
7e1fc13e52 Make watchlist update_discovery_pool_incremental use provider priority
Continuation on recent changes
2026-04-17 09:08:36 +03:00
Antti Kettunen
bc83874c6f Discovery fan-out and playlists follow source priority
Make discovery pool population and curated playlists follow the configured metadata source order. Keep Spotify strict where fallback would corrupt source-specific IDs, and trim fan-out with smaller similar-artist samples and page caps. Leave the remaining incremental path for follow-up.
2026-04-17 08:49:19 +03:00
Antti Kettunen
030374c5b0 Tune discovery fan-out and caching
Reduce request volume in the discovery helpers while keeping the source-priority model intact.

- make cache_discovery_recent_albums source-priority aware
- cap Spotify artist-album pagination in the discovery and incremental paths
- reduce the similar-artist sample size for the cache-refresh helper
- keep Spotify strict where fallback would contaminate source-specific IDs
- add regression coverage for source order, strict Spotify lookups, and pagination caps
2026-04-17 08:27:36 +03:00
Broque Thomas
09d358ef69 Fix watchlist scan false failures, Spotify backfill, and wishlist remove
Watchlist scanner: empty discography (no new releases in lookback) was
treated as API failure, causing "Failed to get artist discography" for
artists like Kendrick Lamar who simply had no recent releases. Now
distinguishes None (API failure → try next source) from [] (success,
no new tracks). Spotify backfill now uses the authenticated client
instance instead of creating a fresh unauthenticated one.

Wishlist nebula: album remove now sends album_name (API updated to
accept album_name as fallback alongside album_id). Track remove
re-renders the nebula after deletion. Toned down processing pulse
animation.

Updated test to verify fallback triggers on API failure (None), not
on empty results.
2026-04-16 18:06:45 -07:00
Antti Kettunen
e447cf6ab0 Reduce discovery fan-out and pagination
Make discovery pool population respect provider priority while keeping Spotify strict, and reduce unnecessary request volume in the hot discovery paths.

- keep discovery fan-out source-priority aware
- preserve cache use where freshness is not required
- cap Spotify artist-album pagination in discovery and cache refresh paths
- keep incremental release checks to a single page, since they only need the newest releases
- add regression coverage for provider order, strict Spotify handling, and pagination caps
2026-04-16 20:59:26 +03:00
Antti Kettunen
e657a1d432 Make watchlist Spotify matching strict
Resolve Spotify artist matching through the exact Spotify client only, so watchlist ID backfill cannot drift to fallback-provider results. Remove the remaining preemptive provider availability check from the backfill loop.
2026-04-16 09:59:49 +03:00
Antti Kettunen
38b907097d Make watchlist scanning source-aware
Move the web watchlist scan core onto the shared metadata source priority so primary provider settings are respected during artist, album, and image resolution.

Add coverage for primary-source-first discography lookup and fallback to later providers when the primary source has no albums.
2026-04-16 09:13:15 +03:00
Antti Kettunen
9d73b8b561 Restore placeholder filtering and shared image backfill
Bring placeholder tracklist skipping back into the shared watchlist scan path, and centralize the DB-only artist image backfill helper so both web scan entrypoints reuse the same logic.
2026-04-16 08:31:04 +03:00
Antti Kettunen
657d86cace Consolidate web watchlist scanning
Move the shared watchlist scan loop into core/watchlist_scanner.py so web_server.py only handles triggers, locks, progress, and post-scan orchestration.

Manual and scheduled watchlist scans now share the same scanner-side core, while the web entrypoints keep profile selection and automation progress updates.
2026-04-16 08:20:48 +03:00
Antti Kettunen
dab58766b7 Make library reorganize source-aware
Respect the configured metadata source order when looking up album years, and re-check provider availability during the scan so Spotify can drop out cleanly if it becomes rate-limited.
2026-04-15 21:35:58 +03:00
Antti Kettunen
03711c10df Make metadata gap filler source-aware
Only fetch source track details when ISRC enrichment is enabled, and show resolved source/track provenance in the UI.
2026-04-15 21:35:57 +03:00
Antti Kettunen
6df6ecb560 Respect source preference in cover art repair job
Cover art lookup now honors an explicit prefer_source first,
falls back to the runtime primary metadata source when unset,
and uses the shared source priority for the remaining fallbacks.
2026-04-15 21:28:12 +03:00
Antti Kettunen
e7faa9f02f Use metadata source priority in track number repair job
Use the shared metadata source priority when resolving album IDs,
album searches, and tracklists in track number repair.

Keeps Deezer and iTunes ahead of Spotify where configured, while
still allowing the job to fall back through other supported sources.
2026-04-15 21:28:12 +03:00
Antti Kettunen
6dca19ca1e Use metadata source priority in unknown artist fixer job
Unknown artist resolution now uses the shared metadata source priority and only filters to the sources that can actually participate in this job. Deezer and iTunes remain direct lookup sources, while Hydrabase can now join the title-search path when it is the configured priority source.
2026-04-15 21:28:10 +03:00
Antti Kettunen
8a1ae00946 Utilize global Spotify client in repair jobs
Album completeness and any other repair job now uses the centralized source/client helpers instead of a worker-local Spotify client or override plumbing
  - This keeps source selection aligned with the configured primary provider and removes the last Spotify-only special case from the job path.

This change ultimately is a step towards further centralizing the Spotify client access and the associated `is_spotify_authenticated` check.
  - Currently these look-ups are done all over the place in different feature implementations directly, but moving forward, any feature that uses `get_primary_client` or `get_client_for_source` to access the Spotify client, won't have to duplicate any rate-limiting or auth checks as long as these getters are used
2026-04-15 07:18:34 +03:00
Antti Kettunen
1a459412a3 Honor primary metadata source in album_completeness job
Album completeness and downstream repair flow now follow the configured
primary provider first, with Discogs and Hydrabase support added alongside
existing Spotify, iTunes, and Deezer paths.

Keep spotify_track_id for compatibility while preserving source-aware track
IDs for provider-neutral handling.
2026-04-14 21:14:46 +03:00
px
07285e17b5 Fix Socket.IO client fallback order 2026-04-14 19:12:32 +09:30
Broque Thomas
71e4df65e3 Remove emojis from all Python log and print statements
Stripped 4,200+ emoji characters from print(), logger calls across
39 Python files. Logs are now clean text — easier to grep, more
professional, no encoding issues on terminals without Unicode support.

Seasonal config icons preserved for UI display.
2026-04-11 21:11:02 -07:00
Antti Kettunen
fd6335a66e Add / improve metadata client caching
Clients are for the most part being initialized per-request, which leads to a lot of redundant client initialization, as well as noise on the logs, since each client initialization emits a row on the logs, eg. 'Deezer client initialized'
2026-04-11 13:55:10 +03:00
Broque Thomas
0f428dc45c Add WebSocket real-time updates with automatic HTTP polling fallback
Migrates 38 HTTP polling loops to WebSocket push events across 6 phases: service status, dashboard stats, enrichment workers, tool progress, sync/discovery progress, and scan status. All original HTTP polling is preserved as automatic fallback — if WebSocket is unavailable or disconnects, the app seamlessly reverts to its previous behavior. Includes 162 tests verifying event delivery, data shape, and HTTP parity. Also fixes a copy-paste bug in Beatport sync error cleanup.
2026-03-03 18:26:29 -08:00