Commit graph

24 commits

Author SHA1 Message Date
Broque Thomas
34ba26f5c8 Persist source IDs at download time + backfill onto tracks on sync
Followup to fix/watchlist-external-id-match. The companion PR closed
the demand side — the watchlist scanner asks for tracks by external IDs
before falling back to fuzzy. But for users on Plex / Jellyfin /
Navidrome the supply side was still broken: tracks.spotify_track_id
(and the other ID columns) only got populated by the asynchronous
enrichment workers, sometimes hours after the file was actually
written. During that window the ID match fell through to fuzzy and
the bug returned.

We were already collecting every ID during post-processing — they
live in the `pp` dict in core/metadata/source.py:embed_source_ids and
get embedded into file tags. We just dropped the in-memory copy
afterwards.

This PR persists them and uses them:

- Schema migration adds spotify_track_id / itunes_track_id /
  deezer_track_id / tidal_track_id / qobuz_track_id /
  musicbrainz_recording_id / audiodb_id / soul_id / isrc columns +
  indexes to the existing track_downloads table (already keyed by
  file_path).
- core/metadata/source.py:embed_source_ids exposes pp["id_tags"] and
  the resolved ISRC back to the import context as _embedded_id_tags
  / _isrc.
- core/imports/side_effects.py:record_download_provenance reads those
  context fields and passes them to db.record_track_download, which
  now accepts the new ID kwargs and persists them.
- New db.get_provenance_by_file_path with exact + basename-suffix
  fallback (handles container mount-root differences between
  download-time path and media-server-reported path).
- New db.backfill_track_external_ids_from_provenance copies IDs
  from track_downloads onto a tracks row idempotently — COALESCE on
  every column preserves any value the enrichment worker already
  wrote (enrichment is more authoritative for late binding).
- database/music_database.py:insert_or_update_media_track (the
  single insertion point used by every Plex / Jellyfin / Navidrome
  sync) calls the backfill immediately after each INSERT/UPDATE.
- New core/library/track_identity.py:find_provenance_by_external_id
  used as a second-tier fallback in watchlist_scanner.is_track_missing
  _from_library — catches the window between download and media-server
  sync. Caller checks os.path.exists on the provenance file_path
  before treating it as "already in library" so a deleted file
  doesn't prevent re-download.

Effect: freshly downloaded files become ID-recognizable to the
watchlist on the very next scan, no enrichment-wait window.

19 regression tests in tests/test_provenance_id_persistence.py:
- Schema migration adds expected columns + indexes
- record_track_download persists every ID kwarg
- record_track_download backward-compat (old kwargs still work)
- get_provenance_by_file_path: exact match, basename fallback for
  mount-root differences, multi-record latest-wins, defensive None
- backfill: copies all IDs, preserves existing via COALESCE,
  no-op when no provenance exists
- find_provenance_by_external_id: per-ID lookup, ISRC cross-bridge,
  OR semantics, latest-wins on multiple matches

Out of scope: backfilling provenance for files downloaded BEFORE
this PR (their track_downloads rows don't carry the new IDs). Those
continue to wait for enrichment. Acceptable — only affects historical
files; new downloads benefit immediately.

Full pytest 1625 passed; ruff clean.
2026-05-02 17:44:10 -07:00
Antti Kettunen
b85a05fb88
Move image URL normalization into metadata helpers
- keep existing /api/image-proxy URLs from being wrapped again
- reuse the shared metadata package instead of duplicating URL logic in web_server.py
- add regression coverage for proxy passthrough and internal URL normalization
2026-05-02 22:02:36 +03:00
Antti Kettunen
36131656dd
Make Spotify status updates event-driven
- move Spotify status publishing onto auth, disconnect, and rate-limit transitions
- keep dashboard and debug consumers on the shared cached snapshot
- leave only the initial snapshot seed as a fallback probe
2026-05-02 22:02:01 +03:00
Antti Kettunen
cc13fb8f01
Move metadata status cache into core/metadata
- move metadata-source and Spotify status caching out of web_server.py
- keep the public /status payload unchanged while shrinking server-side glue
- centralize invalidation and TTL handling in core/metadata/status.py
2026-05-02 22:02:00 +03:00
Antti Kettunen
e2bd0e1871
Split metadata source and Spotify status
- Keep the primary metadata provider snapshot generic and move Spotify auth/rate-limit details into a separate status object.
- Update the websocket fixture and dashboard/settings consumers to read the two buckets independently.
2026-05-02 22:02:00 +03:00
elmerohueso
cd19aa0301 revert tidal artist/track id name for hifi downloads
Co-authored-by: Copilot <copilot@github.com>
2026-05-02 07:56:47 -06:00
elmerohueso
4ddb86522c name tidal and hifi tags the same way 2026-05-02 07:50:13 -06:00
elmerohueso
e78dd7f593 get tidal tags during download, without needing to go through the enrichment pipeline 2026-05-02 07:50:12 -06:00
elmerohueso
1f4e8e5e3b get hifi tags during download, without needing to go through the enrichment pipeline 2026-05-02 07:50:12 -06:00
elmerohueso
b363afe195 bpm for tidal, copyright and bpm for hifi 2026-05-02 07:50:12 -06:00
elmerohueso
f9f47f978e fix post-download tagging, and enable tagging for hifi 2026-05-02 07:50:12 -06:00
Antti Kettunen
74e3cc460c
Simplify service status and labels
- Flatten the Spotify service-status rendering so it shows rate-limit and recovery states explicitly, while otherwise displaying the active metadata provider directly.
- Keep the Spotify auth controls and metadata-source picker aligned with the real session state after authenticate and disconnect flows.
- Return "Unmapped" for unknown metadata source labels instead of implying iTunes.
- Update the metadata registry tests to cover the new label fallback.
2026-05-01 12:06:58 +03:00
Antti Kettunen
55603be14c
Clarify Spotify auth flow and sync UI
- Send Spotify auth completion back to the opener so the settings page refreshes immediately
- Make the local auth flow go straight through to Spotify instead of showing the temporary instruction page
- Keep the remote/docker instruction page available for manual callback setups
- Sync Spotify status, connect/disconnect buttons, and metadata source selection after auth and disconnect
- Keep the disconnect behavior aligned with the active primary metadata source
2026-05-01 11:25:12 +03:00
Antti Kettunen
9646f6ca7f
Clarify Spotify auth actions
- Hide the auth button when a Spotify session is active
- Treat disconnect as a session change, not a provider swap
- Share metadata source labels in the registry
- Tighten rate-limit copy around Spotify-specific behavior
2026-05-01 10:36:50 +03:00
Antti Kettunen
e6c2bee427
Move profile Spotify cache into registry
- let core.metadata.registry own per-profile Spotify client caching
- register the DB-backed profile credentials provider from web_server.py
- invalidate only the affected profile cache entry on save, delete, and auth
2026-04-29 12:36:37 +03:00
Antti Kettunen
11be8834eb
Use metadata registry for web_server clients
- make web_server.py read and refresh Spotify from core.metadata.registry
- add single-key metadata cache eviction for Spotify reauth
- export the new cache helper through the metadata package shims
2026-04-29 12:27:59 +03:00
Antti Kettunen
50e1ae3a3f
Move metadata helpers into package modules
- split metadata lookup logic into core/metadata/*
- keep core/metadata_service.py as the legacy barrel
- update tests and artist-detail code to patch concrete modules
2026-04-29 11:28:42 +03:00
Antti Kettunen
a759f778b6
Move metadata API into package
- add package-owned metadata API, cache, registry, and lookup modules
- keep legacy metadata_service and metadata_cache paths as explicit shims
- update metadata call sites and tests to use package-owned helpers
2026-04-29 08:10:18 +03:00
Broque Thomas
c121582557 MusicBrainz genres: fall back to release then artist when recording is empty
User report: SoulSync was only pulling MusicBrainz genres from the
recording (track-level) endpoint. Most MB recordings don't carry genres
at the track level — they live on the release (album) or artist. So
the MB tier was contributing nothing to the genre merge for the
overwhelming majority of tracks.

Fix:
- Added `'genres'` to the release-detail `includes` (was missing).
- After release-detail processing, if pp['mb_genres'] is still empty,
  populate from release_detail['genres'] (sorted by count desc).
- If still empty AND artist_mbid is set, fetch artist with
  `includes=['genres']` and use those.

No extra API call when the recording (or release) already had genres —
the artist fetch only fires when both upstream tiers came back empty.

The downstream genre merge in _embed_metadata_genres is unchanged; this
just makes the MB feed into it richer.

Tests: 4 new (recording present, recording empty → release, recording
+ release empty → artist, all empty → []). Full suite 873 passing.
Ruff clean.

Reported by @kcaoyef421 in Discord.
2026-04-27 19:31:27 -07:00
Antti Kettunen
02305096a3
Tighten metadata and import safety
- Normalize album import track display handling so queue labels and match rows stay consistent
- Bound MusicBrainz caches and avoid caching transient lookup failures
- Stop swallowing programmer errors in source enrichment helpers
- Restore import config test seams without reintroducing lazy imports
- Guard task completion calls and fix the Windows path test expectation
- Keep file lock tracking from growing without bound
2026-04-27 20:28:05 +03:00
Antti Kettunen
9b2b6d856f
Split runtime builders into owning modules
- Move the import pipeline runtime factory into core.imports.pipeline
- Move the metadata runtime factory into core.metadata.enrichment
- Keep the web server wiring thin and drop the shared glue module
- Add contract tests that keep the two runtime bundles separate
2026-04-27 19:54:45 +03:00
Antti Kettunen
9e496397da
Move shared metadata helpers into package
- Relocate the shared metadata helper module from core/metadata_common.py into core/metadata/common.py.
- Update the new metadata package, the import pipeline, and the web entrypoint to use the package-scoped helper.
- Keep the shared config, mutagen, file-lock, and tag-writing helpers centralized without touching unrelated files.
2026-04-27 19:54:44 +03:00
Antti Kettunen
9656dbd46a
Thread runtime through metadata enrichment
- Pass the live runtime bundle into the shared metadata facade so worker-backed source enrichment can actually run.
- Forward runtime from the import pipeline and web-server wrapper into embed_source_ids.
- Add a regression test that verifies the runtime object reaches the source-ID embedding path.
2026-04-27 19:54:44 +03:00
Antti Kettunen
8319c6679f
Move new metadata helpers into a package
- Keep existing metadata_cache and metadata_service at the top level for now
- Move the new branch-local metadata helpers under core/metadata
- Share MusicBrainz release cache state from core.metadata.source and update import sites
2026-04-27 19:54:44 +03:00