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.
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.
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.
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".
- 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)
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).
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.
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)
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.
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.
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
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).
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.
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
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.
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.
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.
"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.
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.
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)
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
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
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.
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.
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.
- 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
Full stats dashboard that polls Plex/Jellyfin/Navidrome for play
history and presents it with Chart.js visualizations:
Backend:
- ListeningStatsWorker polls active server every 30 min
- listening_history DB table with dedup, play_count/last_played on tracks
- get_play_history() and get_track_play_counts() for all 3 servers
- Pre-computed cache for all time ranges (7d/30d/12m/all) rebuilt each sync
- Single cached endpoint serves all stats data instantly
- Stats query methods: top artists/albums/tracks, timeline, genres, health
Frontend:
- New Stats nav page with glassmorphic container matching dashboard style
- Overview cards (plays, time, artists, albums, tracks) with accent hover
- Listening timeline bar chart (Chart.js)
- Genre breakdown doughnut chart with legend
- Top artists visual bubbles with profile pictures + ranked list
- Top albums and tracks ranked lists with album art
- Library health: format breakdown bar, unplayed count, enrichment coverage
- Recently played timeline with relative timestamps
- Time range pills with instant switching via cache
- Sync Now button with spinner, last synced timestamp
- Clickable artist names navigate to library artist detail
- Last.fm global listeners shown alongside personal play counts
- SoulID badges on matched artists
- Empty state when no data synced yet
- Mobile responsive layout
DB migrations: listening_history table, play_count/last_played columns,
all with idempotent CREATE IF NOT EXISTS / PRAGMA checks.
- Add get_artist_top_tracks to Last.fm client (up to 100 tracks)
- Include lastfm_listeners, lastfm_playcount, lastfm_tags, lastfm_bio,
and soul_id in artist detail API response
- New endpoint: /api/artist/<id>/lastfm-top-tracks for lazy loading
- Hero layout: image (160px) | center (name, badges, genres, bio,
listener/play stats, progress bars) | right card (scrollable top
100 tracks from Last.fm)
- Badges 36px with hover lift, bio in subtle card with Read More
toggle, Last.fm tags merged with existing genres
- Numbers formatted: 1234567 → 1.2M
- Graceful degradation: sections hidden when Last.fm data unavailable
SoulID worker generates deterministic soul IDs for all library entities:
- Artists: hash(name + debut_year) — searches iTunes + Deezer APIs,
verifies correct artist by matching discography against local DB
albums via MusicMatchingEngine, pools years from both sources and
picks the earliest. Falls back to hash(name) if no match found.
- Albums: hash(artist + album)
- Tracks: song ID hash(artist + track) + album ID hash(artist + album + track)
Dashboard button with trans2.png logo, rainbow spinner, hover tooltip.
Worker orb with rainbow effect. SoulSync badge on library artist cards.
DB migration adds soul_id columns with indexes to artists/albums/tracks.
Migration version flag auto-resets artist soul IDs when algorithm changes.
DB migration adds 11 columns to profiles table for per-profile Spotify
credentials, Tidal tokens, and media server library selection. All
encrypted, all default NULL (fall back to global config).
API endpoints follow existing ListenBrainz per-profile pattern:
- GET/POST/DELETE /api/profiles/me/spotify
- GET/POST /api/profiles/me/server-library
Foundation only — no frontend UI or client initialization changes yet.
Preflight: hash track IDs before syncing and compare against last sync.
Skip only if exact same tracks were already synced and all matched.
Replaces the old count-based smart-skip which could miss track swaps.
Sync history: update existing entry for same playlist_id instead of
creating duplicates. Re-syncing the same playlist now refreshes the
existing row with new timestamps and stats.
If album info is missing, not a dict, or named "Unknown Album",
it gets repaired using the track name as fallback instead of
storing junk display data.
INSERT OR REPLACE on existing tracks was deleting the entire row and
reinserting only 9 columns, nuking spotify_track_id, deezer_id, isrc,
bpm, musicbrainz IDs, and ~15 other enrichment columns every scan.
Now uses UPDATE for existing tracks (preserves all enrichment) and
INSERT only for new tracks. Also ensures file_path gets updated from
the media server on each scan, fixing stale paths for users whose
files were moved/reorganized.
- New sync_history DB table tracks last 100 syncs with full cached context
- Records history for all sync types: Spotify, Tidal, Deezer, YouTube,
Beatport, ListenBrainz, Mirrored playlists, and Download Missing flows
- Sync History button on sync page with modal showing entries, source
filter tabs, stats badges, and pagination
- Re-sync button: server syncs expand card inline with live progress bar,
matched/failed counts, and cancel button; download syncs open download modal
- Re-syncs update the original entry (moves to top) instead of creating duplicates
- Delete button (x) on each entry with smooth remove animation
- Fix mirrored playlist source detection (youtube_mirrored_ matched youtube_)
- Fix broken server import thumbnails with URL validation
- Switch user automations to 2-column grid layout (matches system automations)
- Add duplicate button on non-system cards with POST /api/automations/<id>/duplicate
- Add search/filter bar (text search + trigger/action dropdowns) shown at 6+ automations
- Add Inspiration section with 8 starter templates that pre-fill the builder
- Add folder-style automation grouping with group_name DB column, dropdown
popover for assignment, collapsible group sections, and builder group input