Commit graph

219 commits

Author SHA1 Message Date
BoulderBadgeDad
3618f3fa7f
Merge pull request #298 from kettui/fix/respect-metadata-provider-in-album-completeness-job
Honor primary metadata source in album_completeness job and associated repair flow
2026-04-15 07:15:15 -07:00
Broque Thomas
c1ef32acd2 Fix source-info popover showing no data due to path format mismatch
track_downloads stores local Windows paths but tracks table stores
server-side paths (Plex/Jellyfin). Both the track_id lookup (NULL
due to failed auto-link at insert time) and exact file_path fallback
were failing.

Added filename-suffix LIKE matching as a final fallback in
get_track_source_info, plus a back-link so the track_id gets written
back for fast future lookups. Also improved the auto-link in
record_track_download to use the same suffix matching when exact path
fails.
2026-04-14 12:30:39 -07: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
Broque Thomas
86621704fe Fix Discover synced playlists not appearing under Server Playlists
Server Playlists was filtered to only show playlists matching mirrored_playlists entries,
but Discover syncs are stored in sync_history (not mirrored_playlists), so they were
excluded. Adds GET /api/sync/history/names returning distinct synced playlist names,
and includes those in the filter alongside mirrored playlists.
2026-04-14 08:24:44 -07:00
Broque Thomas
3b8b369492 Add Your Albums — multi-source liked albums pool (Spotify, Tidal, Deezer)
Builds a new Your Albums section on the Discover page that aggregates
saved/liked albums from all connected services, mirroring the Your Artists
pattern. Deezer works via both OAuth and ARL.

- tidal_client: add get_favorite_albums() with V2/V1 API fallback
- deezer_client: add get_user_favorite_albums() via OAuth (user/me/albums)
- deezer_download_client: add get_user_favorite_albums() via ARL session
- music_database: add liked_albums_pool table (deduped by artist::album
  normalized key), upsert_liked_album, get_liked_albums,
  get_liked_albums_last_fetch, clear_liked_albums
- web_server: GET /api/discover/your-albums (ownership-checked, paginated),
  GET /api/discover/your-albums/sources, POST /api/discover/your-albums/refresh,
  _fetch_liked_albums background worker (Spotify + Tidal + Deezer OAuth/ARL)
- frontend: Your Albums section with source selector cog, album grid reusing
  spotify-library-card styles, search/filter/sort/pagination, download missing
  button, auto-refresh poll on first load

Also fix: Deezer greyed out in Your Artists sources when using ARL — connection
check now accepts ARL auth (deezer_dl.is_authenticated()) in addition to OAuth,
and _fetch_and_match_liked_artists falls back to ARL client for artist fetching.
2026-04-13 23:02:37 -07:00
Broque Thomas
b4cf8b4cc1 Fix allow_duplicates for same-track-ID across different albums
The wishlist table has a UNIQUE constraint on spotify_track_id, so
INSERT OR REPLACE silently overwrote the existing entry when the same
track appeared on a different album (same Spotify track ID). Now uses
a composite key (track_id::album_id) when allow_duplicates is on and
the base track ID already exists, allowing both versions to coexist.
Only affects users with allow_duplicates enabled.
2026-04-12 19:37:13 -07:00
Broque Thomas
f9450b4fea Purge cached tracks with junk artist names on first v2.3 startup
One-time migration deletes metadata cache entries where artist_name is
null, empty, 'unknown', 'unknown artist', etc. The cache gate now
prevents new junk entries, but existing poisoned data from before the
fix needs cleaning so users don't keep getting Unknown Artist from
stale cache hits.
2026-04-12 16:33:08 -07:00
Broque Thomas
45ba51ce3c Add diacritic-insensitive matching to library artist search
search_artists now uses unidecode_lower() and _normalize_for_comparison()
so 'Tiesto' finds 'Tiësto'. Track search already had this — artist
search was the only gap. No change to stored data, only the comparison.
2026-04-12 15:26:21 -07:00
BoulderBadgeDad
4f25cf4661
Merge pull request #286 from kettui/fix/improve-graceful-shutdown
Improve shutdown procedures so that the application can close gracefully
2026-04-12 12:42:00 -07:00
Antti Kettunen
aec3047216 Improve graceful shutdown and rollback safety
- Add interruptible stop events to background workers so shutdown
  wakes out of long sleeps instead of waiting on fixed delays.
- Stop scan managers, repair worker, executors, and cleanup helpers
  deterministically so process exit does not leave background threads
  alive.
- Add startup warnings for stale SQLite WAL/SHM sidecars so unclean
  shutdowns are easier to spot before init/migration errors cascade.
- Prevent forced kills from leaving SQLite sidecars behind, which
  made rollbacks to older branches fail with malformed database
  errors.
2026-04-12 15:17:18 +03:00
Antti Kettunen
1348de96c1 Fix db initialization
When starting from scratch (no existing .db file), certain db init steps were being skipped. Upon subsequent startup, these remaining 4-5 steps would execute.

Up until recently this has not been much of an issue since all the db init steps were run repeatedly throughout the process' lifetime, but after the init was changed to be only done once per startup, this became more problematic
2026-04-12 12:51:52 +03:00
Antti Kettunen
5a40c185e1 Remove lingering local db file 2026-04-12 11:57:56 +03:00
Broque Thomas
08de91685d Add first-run setup wizard and fix download path reloading
7-step full-screen wizard: Welcome, Metadata Source, Download Source,
Paths & Media Server, Add Artists, First Download, Done. All settings
save to DB identically to the Settings page. Supports all 6 download
sources with inline config and test buttons. First download goes through
the full matched download pipeline with metadata context.

Fixes:
- Download clients (YouTube/HiFi/Tidal/Qobuz/Deezer) now reload
  download_path when settings change instead of caching from init
- watchlist_artists table migrations now include deezer_artist_id and
  discogs_artist_id in all 3 table rebuild locations (was being dropped)
- CREATE TABLE for watchlist_artists includes all provider ID columns
- Serverless download sources (YouTube/HiFi/Qobuz) show green status
  instead of red disconnected on sidebar and dashboard
- Suppress repeated slskd 401 errors — logs once then silences until
  connection recovers
2026-04-11 22:45:16 -07:00
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
Broque Thomas
a3ab5adcba Backfill MusicBrainz recording ID from Navidrome during database scan
Navidrome provides musicBrainzId on tracks — now captured during
database updates so the MusicBrainz enrichment worker can skip
tracks that already have an MBID.

Uses COALESCE on UPDATE to never overwrite existing enrichment data
with NULL (safe for Plex/Jellyfin which don't provide this field).

Inspired by PR #279 — fixed data loss bug in the original where
unconditional UPDATE would erase existing MBIDs.
2026-04-11 18:47:54 -07:00
Broque Thomas
f5ac7c9261 Include track_artist in track search queries for collab/feature matching 2026-04-11 18:13:07 -07:00
Broque Thomas
1e078192f0 Add track_artist column for per-track artist on compilations/DJ mixes
Phase 1: data collection only — no behavior changes.

Adds nullable track_artist column to tracks table. During database
updates (incremental, full refresh, deep scan), extracts per-track
artist from the media server when it differs from the album artist:
- Plex: originalTitle field
- Jellyfin/Emby: ArtistItems[0] vs AlbumArtists[0]
- Navidrome: artist attribute vs album artist name

NULL for normal albums (track artist = album artist). Populated only
when the media server reports a different per-track artist.

UPDATE uses COALESCE to never overwrite existing data with NULL.
2026-04-11 17:12:28 -07:00
Antti Kettunen
337ec7309b Initialize the database only once per process
Since MusicDatabase is initialized per-thread (which I don't dare to change), we end up needlessly calling _initialize_database for each client that gets created, thus making a ton of redundant db initialization / migration calls over and over again throughout the process' lifetime
2026-04-11 14:35:58 +03:00
Broque Thomas
df14bbf745 Add one-time migration to purge stale Deezer metadata cache
Deezer album entries cached from /artist/{id}/albums lack artist info,
and track entries from search results lack track_position. Purges all
Deezer album/track cache entries on first startup so they repopulate
with complete data.
2026-04-10 10:45:47 -07:00
Broque Thomas
dd5291456b Fix playlist pipeline discovery data loss and Unknown Artist bug
Discovery workers now respect the user's configured primary metadata
source instead of always using Spotify when authenticated. This
completes the intent of commit 3c211ea.

The core fix addresses data loss in the discovery→sync→wishlist→download
pipeline: the Track dataclass strips album metadata to a plain string,
losing album ID, track_number, release_date, and images. Discovery
workers now enrich results via get_track_details() to recover this data.
Deezer's get_track_details() cache validation was incorrectly trusting
search-result cache (which lacks track_position), returning track_number=0.

Also fixes wishlist download processing where albums without IDs couldn't
map to artists, and the fallback read 'artist' (singular) instead of
'artists' (plural), always producing "Unknown Artist".

Includes a one-time migration to purge stale discovery cache entries.
2026-04-10 10:23:43 -07:00
Broque Thomas
195484441e Fix all artist names stored as lowercase in database
A @staticmethod _normalize_artist_name (for liked artists dedup)
shadowed the instance method of the same name (for artist import).
The static version lowercased everything, so every artist name was
stored lowercase during database scans. Renamed the static method
to _normalize_artist_name_for_dedup. Existing lowercase names will
be corrected automatically on the next database scan.
2026-04-08 12:49:38 -07:00
Broque Thomas
e65b6bab67 Add metadata source filter to library and fix Discogs enrichment
Library page: new dropdown filter to show artists matched or unmatched
to any metadata source (Spotify, MusicBrainz, Deezer, Discogs, etc).
Select "No Discogs" to find artists needing manual Discogs matching.
Filter applied as WHERE clause on the source ID columns.

Discogs enrichment: added to valid_services whitelist, _enrichment_locks,
and _run_single_enrichment handler. The Enrich button was returning an
error when Discogs was selected from the dropdown.
2026-04-07 09:29:48 -07:00
Broque Thomas
410bddd102 Redesign download history with collapsible entries and full provenance
Entries are now compact cards that expand on click to reveal source
details. Shows expected vs downloaded title/artist with red mismatch
highlighting. Source artist column added to DB. Streaming track IDs
extracted from the id||name filename pattern. File and ID always on
their own line to avoid edge-case misplacement.
2026-04-06 19:33:02 -07:00
Broque Thomas
f8fbcb507c Add source provenance and AcoustID result to download history
Track original source filename, track ID, and AcoustID verification
result for every download. Helps debug wrong-file downloads from
streaming sources like Tidal. Each column migrated independently
for crash safety. Frontend shows source detail line and color-coded
AcoustID badge per entry. Button renamed to "Download History".
2026-04-06 16:18:58 -07:00
Broque Thomas
51a433a558 Fix duplicate artists in search, per-artist name refresh, global search track click
- search_artists() now filters by active media server — no more duplicate
  results from Plex/Jellyfin/Navidrome showing the same artist 3 times
- Per-artist Sync button re-fetches artist name from media server, catches
  renames (e.g., Plex changing "Kendrick Lamar" to "eastside k-boy")
- Global search track click opens download modal directly instead of
  navigating to enhanced search page (matches enhanced search behavior)
2026-04-05 20:41:43 -07:00
Broque Thomas
a2b9e32d04 Add download source tracking to library history modal
New download_source column on library_history table records which source
(Soulseek, Tidal, Qobuz, HiFi, YouTube, Deezer) each track was downloaded
from. Extracted from context username during post-processing.

Frontend shows source badge alongside quality badge on each download entry.
Source breakdown bar below tabs shows per-source totals with color-coded
chips (e.g., "Soulseek: 847 | Tidal: 203"). Includes DB migration for
existing installs. Existing entries show quality only (source is NULL).
2026-04-05 18:03:46 -07:00
Broque Thomas
c336604b71 Fix hero slider + recommended modal returning 0 artists
get_top_similar_artists now accepts require_source parameter to filter
by source ID in SQL. Previously fetched 200 artists then post-filtered,
but cycling logic (last_featured ASC) rotated artists without IDs to
the front, causing all 200 to be filtered out.

Both /api/discover/hero and /api/discover/similar-artists now pass
require_source=active_source so only artists with valid IDs are returned.
2026-04-04 12:19:29 -07:00
Broque Thomas
58d8e830c6 Your Artists on Discover + Deezer OAuth + MB Lookups Manager + Explorer improvements + bug fixes
YOUR ARTISTS (major feature):
- Aggregates liked/followed artists from Spotify, Tidal, Last.fm, Deezer
- Matches to ALL metadata sources (Spotify, iTunes, Deezer, Discogs)
- DB-first matching: library → watchlist → cache → API search (capped)
- Image backfill from Spotify API for artists missing artwork
- Carousel on Discover page with 20 random matched artists
- View All modal with search, source filters, sort, pagination
- Artist info modal: hero image, matched source badges, genres, bio,
  listeners/plays from Last.fm, watchlist toggle, view discography
- Auto-refresh with loading state on first load, polls until ready
- Deduplication by normalized name across all services

DEEZER OAUTH:
- Full OAuth flow: /auth/deezer + /deezer/callback
- Settings UI on Connections tab (App ID, Secret, Redirect URI)
- Token stored encrypted, auto-included in API calls
- get_user_favorite_artists() for liked artists pool

SERVICE CLIENTS:
- Spotify: added user-follow-read scope + get_followed_artists()
- Tidal: get_favorite_artists() with V2/V1 fallback
- Last.fm: get_authenticated_username() + get_user_top_artists()

FAILED MB LOOKUPS MANAGER:
- Manage button on Cache Health modal
- Browse/filter/search all failed MusicBrainz lookups
- Search MusicBrainz directly and manually match entries
- Optimized cache health queries (11 → 4 consolidated)
- Dashboard cache stats now poll every 15s

EXPLORER IMPROVEMENTS:
- Discover button on undiscovered playlist cards
- Status badges: explored/wishlisted/downloaded/ready
- Auto-refresh during discovery via polling
- Redesigned controls: prominent Explore button, icons

BUG FIXES:
- Fix album artist splitting on collab albums (collab mode fed
  album-level artists instead of per-track)
- Fix cover.jpg not moving during library reorganize (post-pass sweep)
- Fix cover.jpg missing when album detection takes fallback path
- Fix wishlist auto-processing toast spam (was firing every 2s)
- Fix media player collapsing on short viewports
- Fix watchlist rate limiting (~90% fewer API calls)
- Configurable spotify.min_api_interval setting
- Better Retry-After header extraction
- Encrypt Last.fm and Discogs credentials at rest
- Add $discnum template variable (unpadded disc number)
2026-04-03 22:39:05 -07:00
Broque Thomas
8b58434c17 Explorer improvements: discover from Explorer, status badges, auto-refresh
1. Discover button on undiscovered playlist cards — triggers discovery
   directly from Explorer instead of redirecting to Sync page. Button
   changes to "Open" to reopen modal after closing.

2. Status badges on playlist cards: checkmark (in library), heart
   (wishlisted), star (fully discovered), percentage (needs discovery).
   Meta line shows "N in library · M wishlisted" counts.

3. Auto-refresh: polls every 5s during active discovery to update cards.
   WebSocket listener for discovery:progress events. Cards refresh when
   discovery completes.

4. Explored tracking: playlists get green checkmark badge after tree is
   built (session-only, resets on reload).

Backend: new get_mirrored_playlist_status_counts with fail-safe design —
core discovery counts use simple reliable queries, library/wishlist
counts are best-effort extras that won't break discovery detection.

Card layout redesigned: badges inline with playlist name, discover
button below meta text, no more absolute positioning overlaps.
2026-04-03 13:00:04 -07:00
Broque Thomas
93fb082172 Add Failed MB Lookups manager + optimize cache performance
New feature: Failed MusicBrainz Lookups management modal accessible
from Cache Health. Browse all failed lookups with type filter tabs,
search bar, pagination. Click any entry to search MusicBrainz and
manually match — saves MBID at 100% confidence. Clear individual
entries or bulk clear all.

Backend: 4 new endpoints — failed-mb-lookups list, mb-entry delete,
musicbrainz/search (artist/release/recording), mb-match save.

Performance: Cache health stats consolidated from 11 queries to 4
using CASE expressions. Added partial index on musicbrainz_cache for
failed lookups. Dashboard cache stats now poll every 15s instead of
single fire-and-forget fetch. Failed MB type counts cached on frontend,
only re-fetched after mutations.

Also includes: library reorganize now moves cover.jpg via post-pass
sidecar sweep, and changelog updates.
2026-04-03 11:38:50 -07:00
Broque Thomas
55f7e174d8 Fix all watchlist Discogs gaps
- Add discogs_artist_id to ALL watchlist WHERE clauses + bind params
- Fix artistPrimaryId to include discogs_artist_id
- Fix View Discography — add Discogs source branch
- Fix openWatchlistArtistDetailView — destructure discogs_artist_id
  from response (was causing ReferenceError)
- Discogs source/provider badges + CSS + image fetch + ID resolution
2026-04-02 21:33:41 -07:00
Broque Thomas
82f9b84e5b Add Discogs to watchlist — column, backfill, matching
- Add discogs_artist_id column to watchlist_artists table (migration)
- Add discogs_artist_id to WatchlistArtist dataclass
- Add to get_watchlist_artists optional_columns and constructor
- Add update_watchlist_discogs_id DB method
- Backfill loop includes Discogs when token is configured
- Add _match_to_discogs for cross-provider artist matching
- Backfill maps updated: id_attr, match_fn, update_fn all include discogs
2026-04-02 20:59:34 -07:00
Broque Thomas
58c3e589b6 Fix artist-hero-badges — add discogs_id to get_artist_discography
- discogs_id was missing from BOTH the SQL SELECT and the artist dict
  in get_artist_discography() — used by the library artist detail page
- This is the third location (after get_library_artists and
  discography endpoint) where discogs_id was in the DB but not
  included in the response
2026-04-02 20:45:31 -07:00
Broque Thomas
cd0e8cf342 Fix Discogs badge gaps — artist_data dict, enrichment coverage, all views
- Add discogs_id to manually constructed artist_data dict in
  get_library_artists (was in SQL but not in response dict)
- Add Discogs to enrichment coverage circles on artist detail page
- Add Discogs to enhanced artist/album ID badges and match status chips
- All badge locations verified: library cards, artist hero, enhanced view
2026-04-02 20:42:09 -07:00
Broque Thomas
930ffbd085 Add Discogs badge to all artist views — fix missing data in queries
- Add discogs_id to library artists SQL SELECT (was missing)
- Add discogs_id to artist detail discography SQL SELECT and service
  IDs loop — fixes hero badges not showing Discogs
- DISCOGS_LOGO_URL constant, badge in library cards, hero, enhanced view
- Match status chip and manual match support for Discogs
2026-04-02 20:03:01 -07:00
Broque Thomas
1455112d40 Move reclassified singles to singles grid, fix collectors edition matching
- Cards reclassified from album to single/EP (via lazy track count)
  now physically move from albums-grid to singles-grid
- Singles section auto-shows when cards move into it
- Add collectors edition to album title variation patterns — fixes
  "Damn" not matching "DAMN. COLLECTORS EDITION." in library
- Both base-title-to-edition and edition-to-base variations now include
  collectors edition alongside deluxe/platinum/special
2026-04-02 19:22:23 -07:00
Broque Thomas
7ea5eb2c06 Fix Discogs completion: lazy track counts, edition matching, type reclassify
- Fetch real track count from source during completion check when
  total_tracks is 0 (Discogs masters) — one API call per album, runs
  during existing per-album ownership check phase
- Reclassify album cards to single/EP when track count reveals 1-3/4-6
  tracks — updates type label and data attribute in place
- Add collectors edition to album title variation patterns for matching
  "Damn" against "DAMN. COLLECTORS EDITION." in library
- Hide 0/0 fraction when expected_tracks is 0, show proper count when
  fetched
2026-04-02 19:02:23 -07:00
Broque Thomas
b68aa09469 Add Discogs enrichment worker with full metadata extraction
- New core/discogs_worker.py — background worker enriching artists and
  albums with Discogs metadata following AudioDBWorker pattern exactly
- Artist enrichment: discogs_id, bio, members, URLs, image backfill,
  genre backfill, summary backfill from bio
- Album enrichment: discogs_id, genres, styles (400+ taxonomy), label,
  catalog number, country, community rating, image backfill
- DB migration: discogs columns on artists (id, match_status, bio,
  members, urls) and albums (id, match_status, genres, styles, label,
  catno, country, rating, rating_count)
- Worker initialization with pause/resume persistence
- Status/pause/resume API endpoints
- Integrated into enrichment status system, rate monitor, auto-pause
  during downloads/scans, WebSocket status emission
2026-04-02 16:57:18 -07:00
Broque Thomas
5d8f3bcaec Store original audio details in track provenance (#245)
- Add bit_depth, sample_rate, bitrate columns to track_downloads table
- Read audio info from file via Mutagen when recording provenance
- Source Info popover shows "Audio: 24-bit · 96.0kHz · 2304kbps"
- These values are captured from the original file before transcoding,
  so users can see the original specs even after Blasphemy Mode converts
  FLAC to lossy format
2026-04-02 15:16:49 -07:00
Broque Thomas
c7d6cc21e8 Preserve track provenance through lossy transcoding (#245)
- Update provenance file_path when Blasphemy Mode deletes the original
  FLAC and replaces it with a lossy copy — provenance now points to
  the transcoded file instead of the deleted original
- New update_provenance_file_path() database method
- Non-blocking: wrapped in try/except, never interrupts transcode flow
- Downsample (hi-res → CD quality) is unaffected — replaces in-place
  with same filename, provenance stays valid
2026-04-02 15:14:27 -07:00
Broque Thomas
f6b0bd30e3 Backfill all metadata source IDs at start of every watchlist scan
- Was only backfilling the active provider — artists added via Deezer
  never got Spotify/iTunes IDs, and vice versa
- Now backfills iTunes (always), Deezer (always), and Spotify (if
  authenticated) at the start of every scan
- Added _match_to_deezer() and update_watchlist_deezer_id() for
  Deezer cross-provider matching
- Generalized backfill with provider→attribute/function maps
2026-04-02 13:51:46 -07:00
Broque Thomas
b194e1e15b Add discovery artist blacklist — block artists from all discovery playlists
- New discovery_artist_blacklist table with NOCASE name matching
- Filter blacklisted artists from all 6 discovery pool queries, hero
  endpoint, and recent releases via SQL subquery and Python set check
- Name-based filtering means one block covers all sources (Spotify/iTunes/Deezer)
- Hover any discovery track row → ✕ button to quick-block that artist
- 🚫 button on Discover hero opens management modal with search-to-add
  (powered by enhanced search) and list of blocked artists with unblock
- CRUD API: GET/POST/DELETE /api/discover/artist-blacklist
- Updated changelogs
2026-04-01 23:35:56 -07:00
Broque Thomas
04f01d36e1 Add track download provenance tracking and source info UI (#234)
New track_downloads table records every download with full source data:
service type (soulseek/youtube/tidal/etc), username, remote filename,
file size, and audio quality. Recorded at all 3 post-processing
completion points.

Source Info button (ℹ) on each track in the enhanced library view shows
a popover with download provenance: service, username, original filename,
size, quality, download date. Includes "Blacklist This Source" button
that stores the real username+filename (not guessed local filenames).

Removed broken "Delete & Blacklist" option from Smart Delete since it
had no access to real source data. Blacklisting now done exclusively
from the Source Info popover where actual provenance data exists.

Added blacklist CRUD API endpoints (GET/POST/DELETE /api/library/blacklist).
2026-04-01 11:37:21 -07:00
Broque Thomas
8d6486bee3 Add Smart Delete with file removal and download blacklist (#234)
Track delete in the enhanced library now shows three options:
- Remove from Library: DB record only (existing behavior)
- Delete File Too: DB + os.remove() the file from disk
- Delete & Blacklist: DB + file removal + add source to blacklist

New download_blacklist table stores rejected sources (username + filename)
with CRUD methods. Blacklist will be checked by the download pipeline
and the upcoming track redownload modal.

Smart delete modal styled with the same glass/dark theme as other
SoulSync modals, with color-coded destructive options.
2026-04-01 10:29:07 -07:00
Broque Thomas
34c8b1bb50 Add Server Playlist Manager with dual-column compare editor
New "Server Playlists" tab (default on Sync page) lets users compare
mirrored playlists against their media server and fix match issues.

- Dual-column comparison: source tracks (left) vs server tracks (right)
- Smart matching: exact title first, then fuzzy artist+title (≥75%)
- Find & Add: search library to fill missing slots at correct position
- Swap: replace matched tracks with different versions
- Remove: delete tracks from server playlists with confirmation
- Title similarity percentage badge on each match
- Disambiguation modal when multiple mirrored playlists share a name
- Album art on source tracks, server tracks, and search results
- Cross-column click-to-scroll highlighting
- Filter buttons (All/Matched/Missing/Extra) with live counts
- Escape key and backdrop click to close modals
- Mobile responsive (stacked columns under 768px)
- Works with Plex, Jellyfin, and Navidrome
2026-03-31 20:58:27 -07:00
Broque Thomas
ad262822a4 Cache per-track results in sync history + fix config_manager import
Sync operations now store per-track data (name, artist, match status,
confidence, download status) in a new track_results column on
sync_history. Also fixed missing config_manager import in
add_to_wishlist that crashed the duplicate tracks toggle.
2026-03-31 14:21:37 -07:00
Broque Thomas
0bc6abd683 Allow duplicate tracks across albums with settings toggle
Same song from different albums was blocked from entering the
wishlist by a name+artist dedup check. Added toggle in Settings →
Library → File Organization: "Allow duplicate tracks across albums"
(on by default). When enabled, the dedup is skipped — different
album versions of the same song can coexist in the wishlist for
complete discography downloads. The UNIQUE constraint on track ID
still prevents the exact same track from being added twice.
2026-03-31 12:16:57 -07:00
Broque Thomas
5334a4688e Preserve discovery data when re-mirroring playlists
mirror_playlist() was deleting all tracks and re-inserting them,
which wiped the extra_data containing discovery results. Now saves
the {source_track_id: extra_data} map before deleting and restores
it on re-insert for tracks that don't bring their own extra_data.
This prevents discovery loss when playlists auto-refresh on tab load.
2026-03-31 10:40:18 -07:00
Broque Thomas
1646c3d9e1 Fix partial name matching false positives (#225)
"Believe" was falsely matching "Believe In Me" because SequenceMatcher
gives high scores when the search string is fully contained in the
match. Added a length ratio penalty: when cleaned titles differ in
length by more than 30%, the similarity score is multiplied by the
ratio (min/max length). This crushes prefix/suffix false positives
while leaving exact matches and cleaned variants (remastered, deluxe)
unaffected.
2026-03-30 17:58:59 -07:00
Broque Thomas
a928522b45 Handle censored track titles from Apple Music in library matching
Apple Music returns censored titles like "B*****t Faucet" for
"Bullshit Faucet". The string similarity function now detects
asterisk patterns and matches by comparing non-censored words
exactly and censored words by prefix/suffix characters.

Only fires when * is present in one string — zero impact on
normal comparisons. Prevents daily re-downloads of explicit
tracks that exist in the library under their uncensored names.
2026-03-27 14:05:18 -07:00