- Added SpotifyId field to Song model
- SquidWTFMetadataService now calls Odesli API when fetching track metadata
- Spotify ID is populated immediately when track is loaded, not during lyrics fetch
- GetLyrics now checks song.SpotifyId first before falling back to cache/Odesli
- Enables Spotify lyrics for all SquidWTF (Tidal) tracks automatically
- Reduces latency - conversion happens once during track load, not every lyrics request
- Changed MaintainWebSocketForSessionAsync to use session.Headers instead of parameter
- Parameter headers might be disposed after HTTP request completes
- Session headers are cloned and stored safely in memory
- WebSocket now properly authenticates as the client instead of falling back to server API key
- Sessions will now appear under correct user in Jellyfin dashboard
- Extract AccessToken from auth response before background task
- Create new HeaderDictionary with token instead of using Request.Headers
- Prevents ObjectDisposedException when HTTP context is disposed
- Session capabilities now work correctly for all clients
Note: WebSocket support for external tracks already implemented via
JellyfinSessionManager.EnsureSessionAsync and WebSocketProxyMiddleware
- Pass through ALL Jellyfin responses (success and error) without modification
- Move session capabilities posting to background task (don't block auth response)
- Remove generic error fallbacks - always return Jellyfin's actual response
- Simplify logic: if Jellyfin returns a response, pass it through; if not, return status code only
- Modified PostJsonAsync to return error response body as JSON when available
- Updated AuthenticateByName to pass through Jellyfin's actual error response
- Clients now see Jellyfin's real error messages instead of generic ones
- Added Library:KeptPath to appsettings.json (default: /app/kept)
- Added Library Settings card to web UI with DownloadPath and KeptPath
- Updated GetDownloads and DeleteDownload endpoints to use configured path
- Updated JellyfinController to use configured kept path
- Injected IConfiguration into JellyfinController
- Users can now customize where favorited tracks are stored
- Changed delete endpoint from Library:DownloadPath to /app/kept
- Now properly deletes empty Album and Artist folders after file deletion
- Added debug logging for deletion operations
- Structure: Artist/Album/Track.flac
- Downloads endpoint now shows both /app/downloads (cache) and /app/kept (favorited)
- Added location field to distinguish between cache and kept files
- Added cacheCount and keptCount to response
- Removed lyrics cache clear endpoint (no longer needed)
- Fix LyricsPrefetchService to use server API key for Jellyfin lyrics checks
- Remove Spotify lyrics caching (local Docker container is fast)
- Disable lyrics prefetching service (not needed - Jellyfin/Spotify are fast)
- Add POST /api/admin/cache/clear-lyrics endpoint to clear LRCLIB cache
- Only LRCLIB lyrics are cached now (external API)
- Handle JsonElement when deserializing ProviderIds from cache
- Check for external provider keys (SquidWTF, Deezer, Qobuz, Tidal)
- Fix row removal selector to properly escape path
- Progress bar now correctly shows local vs external split
- List all downloaded files with artist/album/file info
- Download button to save files locally
- Delete button with live row removal
- Shows total file count and size
- Auto-refreshes every 30 seconds
- Security: path validation to prevent directory traversal
- Fix external track detection in progress bar (check for external provider names in ProviderIds)
- Add missing tracks section at bottom of Active Playlists tab
- Shows all unmatched tracks across all playlists
- Includes Map to Local and Map to External buttons for each missing track
- Auto-refreshes with other playlist data
- Strip decorators FIRST (feat, remaster, explicit, etc)
- Substring matching SECOND (cheap, high-precision)
- Levenshtein distance THIRD (expensive, fuzzy)
- Greedy assignment LAST (optimal global matching)
- Lower threshold to 40 (was 50-60) for max coverage
- Accept artist priority matches (artist 70+, title 30+)
- Handles cases like 'luther' → 'luther (feat. sza)'
- Handles cases like 'a' → 'a-blah' with same artist
- Prevents duplicate assignments across tracks
- Inject Spotify ID into ProviderIds for manually mapped local tracks
- Also add Spotify ID to fuzzy-matched local tracks
- Enables Spotify Lyrics API to work for local Jellyfin tracks
- Fallback to Spotify lyrics when local track has no embedded lyrics
- Allow mapping Spotify tracks to local Jellyfin tracks via JellyfinId
- Supports both local (Jellyfin) and external (provider) manual mappings
- Local mappings take priority over fuzzy matching
- Helps when automatic matching fails for tracks already in Jellyfin library
- Lower matching threshold from 60 to 50 for more lenient matching
- Add fallback to trust provider's top result when artist matches well (>=70)
- Helps match tracks with parentheses, brackets, and stylized titles like 'PiLlOwT4lK'
- Provider search already does fuzzy matching, so trust it when artist is correct
- Implemented semaphore-based rate limiter (10 requests per minute)
- Odesli API allows 10 requests per 60 seconds
- Rate limiter ensures 1 request per 6 seconds maximum
- Prevents API rate limit violations
- Cache still used first (30 day TTL) to minimize API calls
- Removed ALL artist deduplication from search results
- Show both local and external artists with same name (e.g., Taylor Swift + Taylor Swift [S])
- Users can now browse external artist albums not in local library
- Added placeholder image support for missing cover art
- Placeholder served for both Jellyfin and external providers when image is null
- Added logging to SquidWTF artist search to show actual URLs
- Removed temporary documentation files from repo
- TIDAL image URLs already correctly implemented with UUID splitting
Changes:
- SearchItems: No deduplication, sort by relevance score
- SearchHints: No deduplication, show all matches
- GetArtists: No deduplication, show all matches
- GetImage: Returns placeholder.png when image unavailable
- Added GetPlaceholderImageAsync() helper method
- Artists with same name now appear separately (local + external [S])
- Fixed deduplication to keep one local AND one external per name
- Added logging to SquidWTF artist search to show actual URLs
- External artists get [S] suffix to distinguish from local
- Allows users to browse external artist albums not in local library
- TIDAL image URLs already correctly implemented with UUID splitting
- External tracks now create Jellyfin sessions on playback start
- Sessions maintained via WebSocket connections to Jellyfin
- Session activity updated during progress reports
- Sessions auto-cleanup after 50s grace period when playback stops
- Clients playing external tracks now appear in Jellyfin dashboard
- Added comprehensive testing documentation
- Only attempt Spotify lyrics for tracks with valid Spotify IDs (22 chars, no 'local' or ':')
- Add Spotify IDs to external matched tracks in playlists for lyrics support
- Proactively fetch and cache lyrics when playback starts (background task)
- Fix pre-existing SubSonicController bug (missing _cache field)
- Lyrics now ready instantly when requested by client
- Lyrics prefetch now uses playlist items cache which has Jellyfin item IDs
- Directly queries /Audio/{itemId}/Lyrics endpoint (no search needed)
- Eliminates all 401 errors and 'no client headers' warnings
- Priority order: 1) Local Jellyfin lyrics, 2) Spotify lyrics API, 3) LRCLib
- Much more efficient - no fuzzy searching required
- Only searches by artist/title as fallback if item ID not available
- All 225 tests passing
- Added 5-minute file cache for playlist summary to speed up admin UI loads
- Added refresh parameter to force cache bypass
- Invalidate cache when playlists are refreshed or tracks are matched
- Fixed incorrect use of anyProviderIdEquals (Emby API) in Jellyfin
- Now searches Jellyfin by artist and title instead of provider ID
- Fixes 401 errors and 'no client headers' warnings in lyrics prefetch
- All 225 tests passing
- Use data attributes instead of inline onclick to avoid quote escaping issues
- Add event listeners after rendering the table
- Fixes issue where Remove button didn't work due to escaped quotes in onclick attribute
- Added DELETE /api/admin/mappings/tracks endpoint
- Removes mapping from JSON file and Redis cache
- Deletes file if it becomes empty after removal
- Added 'Remove' button to each mapping in web UI
- Enhanced confirm dialog explaining consequences for both local and external mappings
- Supports removing both Jellyfin (local) and external provider mappings
- Allows phasing out local mappings in favor of Spotify Import plugin
- Shows all manual mappings in Active Playlists tab
- Displays summary counts (total, jellyfin, external)
- Table shows playlist, Spotify ID, type, target, and creation date
- Color-coded badges for jellyfin vs external mappings
- Auto-refreshes every 30 seconds
- Helps review mappings before phasing out local ones
- GET /api/admin/mappings/tracks returns all manual mappings
- Shows both Jellyfin (local) and external provider mappings
- Groups by playlist and includes creation timestamps
- Returns counts for jellyfin vs external mappings
- External tracks from playlists now look up their Spotify ID from matched tracks cache
- Enables Spotify lyrics API to work for SquidWTF/Deezer/Qobuz tracks
- Searches through all playlist matched tracks to find the Spotify ID
- Falls back to LRCLIB if no Spotify ID found or lyrics unavailable