Deezer client imports config_manager locally in __init__ and stores
it as self._config, unlike the other clients which import at module
level. The allow_fallback line was referencing the wrong name.
Each streaming source (Tidal, Qobuz, HiFi, Deezer) now has an "Allow
quality fallback" checkbox in Settings. When disabled, the source only
tries the exact quality selected — if unavailable, it skips and lets
the orchestrator try the next source. Default is ON (current behavior).
When missing_tracks was empty (API unavailable at scan time) or the
album was completed between scan and fix, the handler returned a 400
error. Now it re-fetches missing tracks from Spotify/iTunes/Deezer at
fix time and auto-resolves findings where the album is already complete.
Enhanced album reorganize now moves LRC, cover.jpg, folder.jpg and other
sidecar files alongside audio files. Added debug log to library reorganize
repair job showing which template is loaded from config vs default.
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.
Method exists on iTunesClient and DeezerClient but was missing from
HydrabaseClient, causing AttributeError when Hydrabase is active and
Build a Playlist tries to fetch artist images.
Before album tracks start post-processing, pre-populate the MusicBrainz
release cache with ONE verified release. All tracks then hit the cache
during per-track processing and get the same release MBID — no second pass,
no file lock contention, tags correct on first write.
Handles edition name mismatches (Spotify says 'Super Deluxe', MB says
just the base name) by stripping qualifiers and searching again.
Validates track count to pick the right edition. Three download paths
covered: download missing modal, enhanced album, and matched album.
New maintenance job scans albums for tracks with mismatched album names, album
artist names, or MusicBrainz release IDs. These inconsistencies cause Navidrome
and other media servers to split one album into multiple entries. The fix
normalizes outlier tracks to the majority value by rewriting file tags.
Event-triggered automations now receive playlist_id from the triggering event
when the action config doesn't have one set. Fixes silent 'No playlist specified'
failures in Discover/Sync chains. Added debug logging to trace event matching.
- All 9 enrichment workers: stop auto-retrying 'error' status items (was infinite loop)
Only 'not_found' items retry after configured days; errors require manual full refresh
- Cover art dedup: check both 'pending' AND 'resolved' findings to prevent recreation
- Cover art scanner: top-level Spotify rate limit check skips Spotify entirely when
banned, falls back to iTunes/Deezer only, logs once instead of spamming 429s
- 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
Integrates play history data into the discovery algorithm:
- Listening profile: _get_listening_profile() builds user's top artists,
genres, play counts, and listening velocity from the last 30 days
- Artist genre cache: pre-built from local DB for O(1) genre lookups
- Release Radar: +10 genre affinity, +15 artist familiarity, -10 overplay
penalty. Weights rebalanced to 45% recency + 25% popularity + bonuses
- Discovery Weekly: serendipity scoring within tiers — boosts unheard
artists in preferred genres, penalizes overplayed artists
- Recent Albums: adaptive time window (21-60 days) based on listening
velocity — heavy listeners get fresher content, casual listeners more
- New "Because You Listen To" sections: personalized carousels based on
user's top 3 played artists via similar artists + genre fallback
- New endpoint: /api/discover/because-you-listen-to with artist images
- Frontend: BYLT sections with artist photo headers on discover page
- All changes gracefully fall back when no listening data exists
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.
Opus ffmpeg command now uses -map 0:a to extract only the audio stream,
matching the user's working command and preventing picture stream
interference with the encoder. After conversion, cover art is embedded
from the source FLAC using Mutagen: Opus gets METADATA_BLOCK_PICTURE
(base64-encoded per OGG spec), AAC gets MP4Cover. Applied to both
the post-download lossy copy and the repair job fix handler.
The lossy converter fix handler now reads codec and bitrate fresh
from the Settings page when converting, not from what was stored
in the finding details at scan time. Users can change their codec
preference after scanning and Fix All will use the new setting.
The lossy converter fix handler now reads delete_original from its
own job settings (repair.jobs.lossy_converter.settings.delete_original)
instead of the global lossy_copy.delete_original. Defaults to false.
Separate from the per-download Blasphemy Mode toggle in Settings.
New library maintenance job that scans for FLAC files missing a lossy
copy (MP3/Opus/AAC) and creates findings. Fix action converts via
ffmpeg using the configured codec/bitrate from Settings. Supports
Blasphemy Mode (delete original + update DB path). Finding details
store codec/bitrate from scan time for consistency. Disabled by
default, manual-run only, no auto-schedule.
- 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
Orphan detector: add normalized tag matching that strips parentheticals
and brackets (feat. X, [FLAC 16bit], etc.) and tries first-artist-only
for comma-separated artists. Prevents false orphan flags for tracks
like "The Mountain (feat. Dennis Hopper...)" that exist in DB as
"The Mountain". All lookups remain O(1) set operations.
Orphan fix: replace auto-delete with user choice prompt. Single Fix
and Fix All both show modal asking "Move to Staging" or "Delete".
Move to Staging relocates file to import staging folder for proper
re-import with metadata matching. Fix action flows through API
endpoint → repair_worker.fix_finding → _fix_orphan_file handler.
Staging path uses docker_resolve_path for container compatibility.
When searching for "Playing the Angel (Deluxe)", MusicBrainz would
match the standard release because its higher popularity score
outweighed the lower title similarity. Now match_release extracts
version qualifiers (Deluxe, Remastered, etc.) from both the query
and each result, applying a +10 bonus for exact qualifier match,
+5 for partial, and -5 penalty when the query has a qualifier but
the result doesn't. This ensures deluxe downloads get the deluxe
MBID, preventing Navidrome from splitting albums.
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.
- Use correct server request types: 'tracks', 'albums', 'artists',
'artist.albums', 'album.tracks' (were singular, caused timeouts)
- Normalize artists to strings (server may send dicts)
- Use native plugin IDs (iTunes/Spotify) instead of soul_id for
album/artist/track IDs so downstream endpoints can resolve them
- Carry soul_id and plugin_id in external_urls for routing
- Pass plugin param from frontend to server for correct client routing
(iTunes vs Deezer vs Spotify) with isdigit() fallback
- Route source=hydrabase to iTunes client for artist images
- Include external_urls in enhanced search API response
- Reduce WebSocket timeout from 15s to 8s
- Remove stale hydrabase.enabled check, use is_connected() directly
- Add hydrabase to frontend alternate source fetch list
- Normalize Hydrabase artists to strings (server may send dicts),
fixing silent crashes that prevented albums/tracks from appearing
Implements get_track_details, get_album, get_artist, get_artist_albums,
get_track_features, is_authenticated, and reload_config to match the
iTunes/Deezer client interface. Each method tries a specific request
type first then falls back to search if the server doesn't support it.
Preserves existing get_album_tracks (List[Track]) for backward compat.
Post-processing now extracts release year from MusicBrainz, Deezer,
Tidal, Qobuz, and Spotify context (first source wins). Writes
ORIGINALDATE and DATE tags to file, and backfills the album year
in the DB if currently missing. Fixes Library Reorganize showing
blank years for Tidal-only downloads.
Also raises Library Reorganize API year lookup cap from 50 to 200.
- Add debouncedAutoSaveSettings() to moveHybridSource and toggleHybridSource
- Skip unconfigured sources at search time with is_configured() check
- Add get_source_status() to orchestrator, include in settings API response
- Auto-disable unconfigured sources in UI on settings load
- Remove redundant enable checkbox — fallback dropdown is the enable
- Hydrabase option only appears in dropdown when connected
- Connect/disconnect dynamically adds/removes dropdown option
- _is_hydrabase_active checks fallback_source == hydrabase (not config toggle)
- Fallback client returns hydrabase_client when selected, iTunes if disconnected
- Auto-reconnect respects fallback selection for dev_mode handling
- hydrabase added to settings save service list for persistence
- Status shows green Connected on page load when auto-connected
Worker checked self.client.sp (non-None even without Spotify auth due
to fallback) instead of is_spotify_authenticated(). Searched via
iTunes/Deezer fallback, got numeric IDs, rejected them all with
warnings. Now sleeps when Spotify isn't authenticated instead of
making pointless fallback searches.
Pending count queries included NULL-ID rows that _get_next_item filters
out, so pending stayed > 0 even when no processable items remained.
Workers reported running instead of idle, UI never turned green. Added
AND id IS NOT NULL to _count_pending_items across all 9 workers to
match the _get_next_item filter.
Workers would endlessly match the same track because UPDATE WHERE id =
NULL matches 0 rows in SQL. Added AND id IS NOT NULL to all enrichment
queries (individual, batch EXISTS, and batch fetch) across all 9
workers. Also added process-level guard for belt-and-suspenders safety.
Fix Deezer get_track → get_track_details method name mismatch.
New setting: Min Completion % (default 0 = disabled). Skips albums
where the user has fewer than N% of tracks — filters out playlist
imports where a single track exists but the full album was never
intended to be downloaded. Catches real failed downloads (6/12)
while ignoring incidental singles (1/12).
Skip refresh attempt if client_id/secret not configured — clears stale
tokens to stop retry loop permanently. Add 5-minute backoff after
failed refresh to prevent hammering Tidal API every 2 seconds.
Wishlist entries from auto-fill were missing album images, album ID,
track number, disc number, and total tracks. Downloads would have no
cover art and wrong file organization. Now includes full album context
matching the standard wishlist data format.
When suffix path matching fails, reads file tags (title+artist) and
checks against DB tracks. Prevents false orphan detection from path
mismatches in Docker where DB paths differ from filesystem paths.
Only runs for files that fail suffix matching — zero overhead in
normal cases.
- Only emit batch_complete automation event when successful_downloads > 0
- Remove int() cast on album_id in _fix_incomplete_album — Plex/Jellyfin
use hash/text IDs that SQLite stores as-is in INTEGER columns
- Use string comparison for album_id equality check
Finding was created with entity_id=None (file-based) but fix handler
required entity_id for DB update. Rewrote handler to work with file_path
as primary — writes corrected track number to file tags, renames file
if needed, updates DB path. Also stores total_tracks in finding details
for correct tag writing.
Replace fixed primary/secondary hybrid dropdowns with an ordered list
of all 5 download sources. Users enable/disable each source and reorder
with up/down arrows to set download priority. Sources are tried in
order until one returns results.
- New hybrid_order config field (backward compat with legacy primary/secondary)
- Download orchestrator loops ordered list with per-source error handling
- Sortable source list UI with icons, toggle switches, priority numbers
- Source-specific settings shown for all enabled hybrid sources
- Seamless migration from legacy 2-source to N-source format
- Add max_peer_queue setting to skip peers with long queues (soft filter
with fallback to unfiltered if all results removed)
- Add download_timeout setting replacing hardcoded 10-minute limit
- Include quality_score (peer health: upload speed, free slots, queue
length) in result ranking — was calculated but never used in sort key
- New UI controls in Soulseek settings section
Was Spotify-only — users without Spotify got zero results. Now queries
albums with any source ID (spotify_album_id, itunes_album_id, deezer_id)
and uses the matching API client for track count and missing track lookup.
Falls back gracefully across sources with client-type detection.
When >50% of files are flagged as orphans (likely a DB path mismatch),
findings are marked as warnings with mass_orphan flag. Fixing these
requires typing "witness me" to confirm — prevents nuking an entire
library from a false-positive orphan scan.
_sanitize_context_values passed empty strings through _sanitize_filename
which converts '' to '_' (empty-name fallback). Template $album ($year)
became Album (_) instead of Album () which the cleanup regex couldn't
match. Now preserves empty strings so the existing () cleanup works.
- Add _fix_path_mismatch handler so fixing Library Reorganize dry-run
findings actually moves files (was returning 400 with no handler)
- Add path_mismatch to fixable_types and _execute_fix handler map
- Add recent_releases and wishlist_tracks as year sources in
_load_album_years to cover more playlist-synced tracks missing years
- Add sys and json imports needed by new code
- Add single_album_redundant to fixable_types in bulk_fix_findings so
Fix All actually includes these findings (Fix Selected worked, Fix All
silently returned 0)
- Expand version keyword regex from 9 to 25 terms (remastered, deluxe,
unplugged, etc.) to reduce false positives in Single/Album Dedup
- Add word boundary anchors to prevent substring matches (e.g. "live"
inside "Alive", "edit" inside "Meditate")
- Cast similarity thresholds to float for config type safety
The SQL HAVING clause filtered on local track count instead of expected
track count, excluding albums with fewer than 3 local tracks from the
scan entirely. Now fetches all Spotify-matched albums and filters by
expected track count in the loop.
- Frontend was concatenating Track and Artist inputs into a single
query string, causing Spotify to return mixed results matching
either word in any field. Now sends track and artist as separate
params; backend builds field-filtered query (track:X artist:Y).
- Result limit was silently capped at 10 in spotify_client.search_tracks
via min(limit, 10). Raised to respect requested limit up to
Spotify's API max of 50.
- iTunes fallback endpoint updated with same field-specific params.
- Legacy ?query= param still supported for backward compatibility.
Fixes#194
EPs are no longer classified as singles — removing EP tracks would
break the release and conflict with album completeness checks.
Only actual singles (album_type='single' or unknown type with <=2
tracks) are flagged as redundant when the same song exists on an album.
Tracks on different albums (e.g., same song on a studio album and a
compilation) are no longer flagged as duplicates by default, keeping
albums complete. Setting is enabled by default and configurable via
the job settings UI.
Playlist-synced tracks often have no year in file tags or the DB albums
table. Now checks discovery_pool release_date as a second DB source,
then batch-lookups remaining missing years from the user's active
metadata source (Spotify/iTunes/Deezer) capped at 50 API calls.
Multiple code paths built wishlist album data as a bare string instead
of a dict with images/album_type/total_tracks, causing wishlist entries
to lose album name and artwork. Fixed in: Beatport fallback conversion,
sync normalization, _ensure_spotify_track_format, and wishlist service
track reconstruction. Now uses track name as album fallback and always
includes required album dict keys.
New dropdown in Soulseek settings lets users filter out slow peers at
search time (Any/1/2/3/4/5/10 Mbps). Passes minimumPeerUploadSpeed
to slskd API in bytes/sec.
Also fixes quality scoring tiers which were using wrong units — old
thresholds (5000, 1000, 500) treated bytes/sec values as kbps,
making speed scoring effectively meaningless. Now uses correct
bytes/sec thresholds based on real peer data.
Deep scan passed skip_existing_tracks=True which skipped calling
insert_or_update_media_track entirely for known tracks, so stale
file paths were never refreshed from the media server. Now always
calls insert_or_update (which safely uses UPDATE for existing tracks,
preserving enrichment data) so file paths stay in sync.
Normalization was stripping parenthetical content before comparison,
so 'title' and 'title (xxx remix)' both became 'title' and always
matched at 1.0 regardless of the user's threshold setting.
_get_source() hardcoded 'itunes' as fallback, so seasonal content was
stored with source='itunes' but API endpoints queried source='deezer' —
resulting in empty discover page seasons. Now reads the configured
fallback source from metadata_service.
Dead file Fix button now adds the track to wishlist for re-download
instead of just removing the DB entry. Builds full wishlist-compatible
track data from DB (artist, album, artwork, IDs) so the download
pipeline can process it like any other wishlist item.
- Add null guard for soulseek_client in test-connection endpoint (#190)
- Fix single_album_dedup query: use al.record_type and al.track_count
(al.album_type and al.total_tracks don't exist on main albums table)
- Fix library reorganize missing year: pre-load album years from DB as
fallback when file tags lack the date field (common with playlist syncs)
- Fix enrichment progress never updating: remove `continue` that skipped
progress_callback for successful tracks in enrich_chart_tracks
- Split chart/extract into two-step flow: extract raw tracks, then enrich
via polling endpoint with live progress overlay updates
- Move Beatport enrichment cache to persistent metadata cache system
- Fix metadata cache detail modal for Beatport (URL entity_ids with slashes)
- Add per-source Clear dropdown (Spotify/iTunes/Deezer/Beatport) to cache browser
- Remove debug logging from enrichment progress tracking
When the maintenance worker flags an incomplete album, users can now
click "Auto-Fill" to automatically locate missing tracks in the library,
move/copy them into the album folder, and apply full metadata enhancement
(MusicBrainz, Deezer, cover art, etc.). Singles are moved; tracks from
multi-track albums are copied. Quality gate prevents filling FLAC albums
with lossy files. Tracks not found in library are added to wishlist with
album context for auto-download.
Both clients have their own Track class separate from iTunes. The
album preference commit added album_type/total_tracks to from_*_track()
constructors but not to the class definitions, crashing all Deezer and
Spotify discovery searches.
Scans library for tracks on singles/EPs that also exist on a full album,
letting users clean up fragmented libraries. Includes version-tag safety
(live, acoustic, remix etc. won't cross-match) and verifies the album
version still exists before removing a single.
Add album_type and total_tracks fields to Track dataclass, populate from
Spotify/iTunes/Deezer API responses, and apply a small tiebreaker bonus
(+0.02 for albums, +0.01 for EPs) in all matching loops so album versions
win when confidence scores are otherwise equal.
Deezer search results (cached without release_date, track_position, isrc)
were being served by get_track_details as cache hits, preventing the actual
/track/{id} API call that returns complete data. This caused $year template
variable to remain empty even with the discovery worker fix in place.
trigger_library_scan and is_library_scanning were no-ops assuming
Navidrome auto-detects new files. Navidrome supports startScan and
getScanStatus via the Subsonic API, so use them like Plex and Jellyfin.
- DB path update uses suffix matching when exact match fails, so server
paths (/mnt/musicBackup/...) are found and updated after local moves
- Escape LIKE wildcards in artist/album names to prevent false matches
- Move album-level sidecars (cover.jpg, folder.jpg, etc.) alongside audio
- Post-pass sweep moves all remaining sidecars from emptied directories
- Multi-disc albums place album art in album root, not disc subfolders
Use config_manager for transfer path (same source as playback) instead of
separate DB read that silently falls back to ./Transfer under contention.
Abort scan if transfer folder doesn't exist to prevent mass false findings.
- personalized_playlists._get_active_source() now returns 'deezer' when
configured instead of always falling back to 'itunes'
- Add deezer_track_id to _build_track_dict() for discovery pool tracks
- Include album_deezer_id and artist_deezer_id in get_discovery_recent_albums()
response — fixes "No deezer album ID available" error when clicking cards
- Skip Spotify library section entirely when Spotify is not authenticated
Per-job and per-status filtering — respects active toolbar filters so
users can clear e.g. only track number findings or only dismissed items.
Confirmation uses the app's styled modal instead of browser confirm().
Previously only 502/503/504 triggered instance rotation. A 500 from one
instance (e.g. triton.squid.wtf choking on a specific query) would stop
the search entirely instead of trying the next instance.
Users can now choose between iTunes/Apple Music and Deezer as their free
metadata source in Settings. Spotify always takes priority when authenticated;
the fallback handles all lookups when it's not.
Core changes:
- DeezerClient: full metadata interface (search, albums, artists, tracks)
matching iTunesClient's API surface with identical dataclass return types
- SpotifyClient: configurable _fallback property switches between iTunes/Deezer
based on live config reads (no restart needed)
- MetadataService, web_server, watchlist_scanner, api/search, repair_worker,
seasonal_discovery, personalized_playlists: all direct iTunesClient imports
replaced with fallback-aware helpers
Database:
- deezer_artist_id on watchlist_artists and similar_artists tables
- deezer_track_id/album_id/artist_id on discovery_pool and discovery_cache
- Full CRUD for Deezer IDs: add, read, update, backfill, metadata enrichment
- Watchlist duplicate detection by artist name prevents re-adding across sources
- SimilarArtist dataclass and all query/insert methods handle Deezer columns
Bug fixes found during review:
- Similar artist backfill was writing Deezer IDs into iTunes columns
- Discover hero was storing resolved Deezer IDs in wrong column
- Status cache not invalidating on settings save (source name lag)
- Watchlist add allowing duplicates when switching metadata sources
Add minimum 60% title similarity gate to match_recording() — prevents
artist bonus + MB score from pushing unrelated titles past the confidence
threshold (e.g. "Sweet Surrender" matching "Answers" by same artist).
New MBID Mismatch Detector repair job reads embedded MusicBrainz recording
IDs from audio files, verifies them against the MusicBrainz API, and flags
tracks where the MBID points to a different song. Fix action strips the bad
MBID tag so media servers like Navidrome fall back to correct file tags.
New download mode alongside Soulseek, YouTube, Tidal, and Qobuz. Uses
community-run REST API instances (no auth required) that serve Tidal CDN
FLAC streams. Features quality fallback chain (hires→lossless→high→low),
automatic instance rotation on failure, and full hybrid mode support.
Also fixes 6 missing streaming source checks for HiFi and Qobuz in the
frontend that were blocking playback with "format not supported" errors.
Scrapes Spotify's embed endpoint to extract track data from any public
playlist or album URL. Full discovery flow with Deezer parity: parse →
card → discovery modal → live progress → sync → download missing.
- New scraper: core/spotify_public_scraper.py (embed endpoint parsing)
- 12 API endpoints mirroring Deezer's discovery/sync/download flow
- WebSocket live discovery updates via spotify_public_discovery_states
- Green-branded tab, cards, and input styling (#1DB954)
- Album vs playlist detection with distinct card icons (💿/🎵)
- Download persistence: card click reopens download modal after close
- Card phase reset on cancel/completion (closeDownloadMissingModal)
- Backend download linking for spotify_public_ and deezer_ prefixes
- Completion handlers (V2 + no-missing-tracks) for both platforms
- Source URLs stored on mirrored playlists for future auto-refresh
Southern hemisphere users now see correct seasons (e.g. March = Autumn,
December = Summer). Holidays (Halloween, Christmas, Valentine's) stay
calendar-fixed regardless of hemisphere.
- Hemisphere dropdown in Discovery Pool Settings
- GET/POST /api/discovery/hemisphere endpoints
- Season detection offsets months by 6 for southern hemisphere
- Stored in metadata table, defaults to northern
Repair jobs were making Spotify API calls without checking the global
rate limit ban, causing them to churn through every item getting rejected
one by one during a ban. Now all Spotify calls in repair jobs check
context.is_spotify_rate_limited() first and skip/fallback gracefully.
- Add is_spotify_rate_limited() helper to JobContext (base.py)
- Guard calls in track_number_repair, album_completeness,
missing_cover_art, and metadata_gap_filler
- Jobs fall through to iTunes/MusicBrainz fallbacks when rate-limited
- All 9 repair jobs now emit report_progress() for real-time card updates
(phase, log lines, per-item activity) via WebSocket repair:progress events
- Enrich finding details with album/artist thumb URLs across all repair jobs
(dead_file, duplicate, metadata_gap, album_completeness, missing_cover_art,
acoustid_scanner, track_number_repair, fake_lossless, orphan_file)
- Track number repair: return match_score from fuzzy matching, add suffix-based
DB lookup for album/artist art (handles cross-environment path mismatches)
- Fix Plex/Jellyfin relative thumb URLs in findings endpoint via fix_artist_image_url
- Labeled media cards in finding detail panels (album title + artist name under images)
- Dashboard tooltip shows current job name + per-job progress instead of stale stats
- Add whisoul.png to modal header with subtitle text and gradient background
- Responsive: smaller logo on mobile, hide subtitle
- Share _resolve_file_path in repair_worker for cross-environment path compat
- Use path resolution in orphan and duplicate file deletion
- Guard directory cleanup against removing the transfer folder itself
- Restore correct button label text on fix error recovery
- Add findings dashboard with summary stats and per-job clickable filter chips
- Redesign findings cards with expandable detail panels and per-type renderers
- Redesign history tab with status dots, stat pills, and full timestamps
- Fix dead file cleaner false positives by using suffix-based path resolution
- Fix orphan file detector false positives by matching via path suffixes
- Add help text modal for each repair job card
- Enlarge maintenance modal (1100px wide, 90vh tall)