Commit graph

208 commits

Author SHA1 Message Date
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
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
Broque Thomas
7f9755a26e Add album-aware track matching for multi-artist albums
When artist-specific track search fails, falls back to album-aware
matching: finds the album by title (any artist), then checks if the
track exists on it. Fixes daily re-downloads of collaborative albums
filed under a different artist (e.g., "Spiral Staircases" tagged
under "The Alchemist" but scanned from "Larry June's" watchlist).

- check_track_exists: new album parameter, album-aware fallback with
  0.8 album title threshold + 0.7 track title threshold
- Watchlist scanner: passes album_data.get('name') to track checks
- Download modal: passes batch_album_context to fallback track search
- Wishlist callers (4 spots): extract and pass track album name
- Backwards compatible: album=None default, no change for callers
  without album context (singles, playlists)
2026-03-27 11:44:01 -07:00
Broque Thomas
a0b2fa9441 Fix false album completion badges, add multi-artist album matching
Completion accuracy:
- Exact match only: "Complete" requires owned_tracks >= expected_tracks,
  no more 90% rounding that hid missing tracks
- Deduplicate track counting: DISTINCT (title, track_number) prevents
  duplicate album entries from inflating owned count (e.g., 3 "GNX"
  entries with 12+1+2 rows counted as 12 unique tracks, not 15)
- MAX(track_count) instead of SUM for stored count — uses largest
  album entry rather than summing duplicates
- file_path IS NOT NULL filter ensures only real files are counted
- Frontend uses real numbers instead of overriding missing=0 when
  backend says "completed"

Multi-artist albums:
- Title-only fallback search when artist-specific search fails
- Finds "Anger Management" filed under "The Alchemist" when checking
  from Rico Nasty's page
- Same confidence scoring prevents false matches
2026-03-27 09:25:08 -07:00
Broque Thomas
a33f891fa6 Add per-artist watchlist lookback period override
New "Scan Lookback" dropdown in the watchlist artist config modal.
Each artist can override the global lookback period (7d to entire
discography). Default is "Use Global Setting" (NULL in DB).

- Database: lookback_days INTEGER DEFAULT NULL on watchlist_artists,
  auto-migrated on startup
- Scanner: checks per-artist lookback_days first, falls back to
  global discovery_lookback_period if NULL
- Backend: GET/POST /api/watchlist/artist/<id>/config includes
  lookback_days. Changing lookback clears last_scan_timestamp to
  force a rescan with the new window
- Frontend: dropdown with 8 options in artist config modal
- Fully backwards compatible — existing artists unchanged
2026-03-27 08:17:02 -07:00
Broque Thomas
6c3b9ddfc2 Optimize watchlist card CSS, backfill missing album covers
Watchlist cards: removed spring-bounce transitions, staggered
animations, and multi-layer hover shadows. Added CSS containment
and will-change for smoother scrolling.

Recent releases: backfill missing album cover art on page load
via metadata source lookup. Persists found covers to database.
2026-03-25 18:36:45 -07:00
Broque Thomas
46308d8d31 Fix watchlist artist image not saving for Deezer source
Two bugs preventing Deezer artist images in the watchlist:

1. web_server.py: The image fetch called get_artist() which doesn't
   exist on DeezerClient. Replaced with direct Deezer API call for
   picture_xl — simple and reliable.

2. music_database.py: update_watchlist_artist_image() only matched
   spotify_artist_id and itunes_artist_id in the WHERE clause.
   Deezer artists use deezer_artist_id, so the UPDATE matched zero
   rows and the image was never saved. Added deezer_artist_id to
   the WHERE clause.
2026-03-25 17:49:33 -07:00
Broque Thomas
e8a5e253f6 Rebuild artist SoulID: track-verified canonical ID from Deezer + iTunes
New algorithm: pick first track alphabetically from artist's library, search
both Deezer and iTunes APIs for 'artist track' to find the exact artist,
verify name matches, then use max(deezer_id, itunes_id) as the canonical
differentiator. Deterministic across instances — any SoulSync with the same
artist and at least one matching track will produce the same soul_id.

Fallback chain: canonical API ID → first album title → name only.
Migration clears all artist soul_ids on first v2.1 startup for regeneration.
2026-03-23 18:34:36 -07:00
Broque Thomas
d8217d66ba Speed up metadata cache browser: add composite indexes, remove full-scan LIKE filters 2026-03-22 23:13:34 -07:00
Broque Thomas
e3d70da55a Add DB storage visualization + cache-powered discovery sections + Genre Deep Dive
- Stats page: database storage donut chart with per-table breakdown and total size
- Discover page: 5 new sections mined from metadata cache (zero API calls):
  Undiscovered Albums, New In Your Genres, From Your Labels, Deep Cuts, Genre Explorer
- Genre Deep Dive modal: artists (clickable → artist page), popular tracks,
  albums with download flow, related genre pills, in-library badges
- All cache queries filtered by active metadata source (Spotify/iTunes/Deezer)
- Stale cache entries (404) gracefully fall back to name+artist resolution
- Album cards show "In Library" badge, artist avatars scaled by prominence
2026-03-22 21:35:18 -07:00
Broque Thomas
21d7e65986 Speed up library page: split DB query, innerHTML rendering, staggered card animation 2026-03-22 16:14:28 -07:00
Broque Thomas
f6225ec9a8 Fix enrichment coverage: correct Spotify column name and add all 9 services 2026-03-22 15:36:25 -07:00
Broque Thomas
9e75731f6c Add scrobbling to Last.fm/ListenBrainz + update What's New
Scrobbling:
- Last.fm: API signing, web auth flow, batch scrobble (50/request)
- ListenBrainz: submit_listens (batch 1000, listen_type=import)
- Worker hook: scrobbles unscrobbled events after each poll
- DB: scrobbled_lastfm/scrobbled_listenbrainz tracking columns
- Settings: API secret input, authorize button, scrobble toggles
- Config: lastfm.api_secret, lastfm.session_key, *.scrobble_enabled

What's New: added all features from this session (scrobbling,
personalized discovery, stats page, SoulID, lossy codecs, import,
hero redesign, Hydrabase, orphan fixes, year collection).
2026-03-22 14:38:53 -07:00