- Changed from fetchLibraryPlaylists to libraryV3 operation (correct Spotify GraphQL endpoint)
- Use GET request with query params instead of POST (matches Jellyfin plugin implementation)
- Updated response parsing to match libraryV3 structure (me.libraryV3.items[].item.data)
- Fixed owner field to use 'username' instead of 'name'
- This should resolve the BadRequest (400) errors when fetching user playlists
- Replace direct REST API calls with SpotifyApiClient GraphQL method
- Add GetUserPlaylistsAsync() method to fetch all user playlists via GraphQL
- GraphQL endpoint is much less rate-limited than REST API /me/playlists
- Enhanced playlist data with track count, owner, and image URL from GraphQL
- Simplified AdminController code by delegating to SpotifyApiClient
- Fix: Use correct HttpClient (_webApiClient) for GraphQL library playlists endpoint
- Was using _httpClient which pointed to wrong base URL causing 429 errors
- Add: Retry logic with Retry-After header support for 429 responses
- Add: Minimum 500ms delay between library playlist pages to prevent rate limiting
- Add: 5-second timeout per endpoint benchmark ping to prevent slow endpoints from blocking startup
- Add: Documentation for timeout requirements in EndpointBenchmarkService
- Fix: ARM64 compatibility for spotify-lyrics service via platform emulation in docker-compose
- Switched from REST API /me/playlists to GraphQL fetchLibraryPlaylists
- GraphQL endpoint is less aggressively rate-limited by Spotify
- Fixes 429 errors when using 'Select from My Playlists' dropdown
- Background services already use GraphQL and work fine
- Added Cronos package for cron expression parsing
- Each playlist now has independent cron schedule (default: 0 8 * * 1)
- Cache persists until next cron run, not just cache duration
- Prevents excess Spotify API calls - only refreshes on cron trigger
- Manual refresh still allowed with 5-minute cooldown
- Added 429 rate limit handling for user playlist fetching
- Added crontab.guru link to UI for easy schedule building
- Both SpotifyPlaylistFetcher and SpotifyTrackMatchingService use cron
- Automatic matching only runs when cron schedule triggers
Each playlist now has its own cron schedule for syncing with Spotify. Default is 0 8 * * 1 (Monday 8 AM). Removed global MatchingIntervalHours in favor of per-playlist scheduling.
Enhanced admin configuration UI with missing fields, required indicators, and sp_dc warning. Added Spotify playlist selector for linking with auto-filtering of already-linked playlists. Configured forwarded headers to pass real client IPs from nginx to Jellyfin. Improved track view modal error handling.
- Change DOWNLOAD_PATH to Library__DownloadPath (ASP.NET Core standard)
- Add EnvMigrationService to automatically update old .env files on startup
- Update .env.example with new variable name
- Ensures cache cleanup and downloads use consistent path configuration
- No manual intervention needed - migration happens automatically
- Track lock state to prevent double-release in finally block
- Fixes exception when download is already in progress
- Prevents duplicate file downloads with (1), (2), (3) suffixes
- Ensures proper lock management during concurrent download requests
- Update CacheCleanupService to use downloads/cache instead of /tmp/allstarr-cache
- Matches the actual path used by download services
- Fixes cache files not being cleaned up after expiration
- Add ArtistIds list to Song model to store IDs for all artists
- Update SquidWTF ParseTidalTrack and ParseTidalTrackFull to populate ArtistIds from artists array
- Update Deezer ParseDeezerTrackFull to populate ArtistIds from contributors
- Update JellyfinResponseBuilder to use real ArtistIds instead of fake IDs
- Fixes UnprocessableEntity errors when clicking on secondary artists
- Enables proper navigation to all artist pages in Jellyfin clients
- Cache mode now uses downloads/cache/ instead of cache/Music/
- Permanent mode now uses downloads/permanent/ instead of downloads/
- Kept files already use downloads/kept/
- All download paths now unified under downloads/ base directory
- Changed from separate paths to unified structure under downloads/
- Structure: downloads/{permanent,cache,kept}/
- Removed Library:KeptPath config, now uses downloads/kept/
- Updated AdminController and JellyfinController to use new paths
- Web UI will now correctly show kept tracks in Active Playlists tab
- Matches user's actual folder structure on server
- Simplified SpotifyMissingTracksFetcher to remove complex sync window timing
- Now fetches on startup if cache missing, then checks every 5 minutes for stale cache (>24h)
- Removed SYNC_START_HOUR, SYNC_START_MINUTE, SYNC_WINDOW_HOURS from config
- Updated README and .env.example to reflect simpler configuration
- Sync window was only relevant for legacy Jellyfin plugin scraping method
- When using sp_dc cookie method (recommended), this service is dormant anyway
- Deleted MIGRATION.md (local-only file, not for repo)
- Health checks run in parallel with 3 second timeout
- Results cached for 30 seconds to avoid excessive checks
- Healthy endpoints tried first, unhealthy ones as fallback
- Prevents wasting time on dead endpoints (no more 5 min waits)
- Failed requests mark endpoint as unhealthy in cache
- Significantly improves response time when some endpoints are down
- Download service was still using default 100s timeout
- Large artist responses and slow endpoints were timing out
- Now both MetadataService and DownloadService use 5 minute timeout
- Fixes 'The request was canceled due to the configured HttpClient.Timeout of 100 seconds' errors
- Some artists have 100+ albums causing large API responses
- Default 100s timeout was insufficient
- Now set to 5 minutes to handle even the largest artist catalogs
- Prevents timeouts when fetching artists like Taylor Swift, etc.
- After Odesli converts Tidal ID to Spotify ID, immediately fetch lyrics
- Lyrics are cached and ready when client requests them
- Happens in background, doesn't block streaming
- Ensures best user experience with instant lyrics availability
- Override ConvertToSpotifyIdAsync in SquidWTFDownloadService
- Odesli API call now happens AFTER stream starts returning to client
- Reduces initial streaming latency by ~3-4 seconds
- Lyrics still work - Spotify ID is cached for on-demand lyrics requests
- Background conversion happens just-in-case for future lyrics needs
- Added 6 new monochrome.tf endpoints (eu-central, us-west, arran, api, samidy)
- Added https variant of hund.qqdl.site (was only http before)
- Total endpoints increased from 10 to 16 for better load distribution
- Optimized Odesli/Spotify ID conversion with 2-second timeout
- If Odesli is slow, download proceeds without waiting (Spotify ID added in background)
- This reduces download time by up to 2 seconds when Odesli is slow
- Spotify ID still obtained for lyrics, just doesn't block streaming
Performance improvement: Downloads that took 9.7s may now complete in 7.7s
- Reduced wait interval from 500ms to 100ms when waiting for in-progress downloads
- Added cancellation token checks during wait loops to handle client timeouts immediately
- Added detailed timing logs to track download performance
- Better error messages when downloads fail or are cancelled
- Prevents OperationCanceledException when client times out (typically 10 seconds)
This fixes the issue where concurrent requests for the same track would timeout
because they were waiting too long for the first download to complete.
- Remove broken JellyfinAuthFilter that was checking non-existent CLIENT_USERNAME
- Clients now authenticate directly with Jellyfin (transparent proxy model)
- Improved token expiration detection and session cleanup
- Better logging with reduced verbosity (removed emoji spam)
- Added support for X-Emby-Token header format
- Added detection of public endpoints that don't require auth
- SessionManager now properly detects 401 responses and removes expired sessions
- Clarified .env.example comments about server-side vs client-side auth
- All functionality preserved: Spotify injection, external providers, playback tracking
- Change WebSocket logs to Debug/Trace (connection events, message proxying)
- Change session management logs to Debug (creation, removal, capabilities)
- Change auth header forwarding logs to Debug
- Change playback forwarding logs to Debug
- Change 401 responses to Debug (expected when tokens expire)
- Keep Info level for significant business events (external playback, downloads)
- Keep Warning/Error for actual issues
This significantly reduces log noise while maintaining visibility of important events.
- Log when GetLyrics endpoint is called with itemId
- Log external vs local track determination
- Log Jellyfin lyrics check status code and result
- Will help identify why embedded lyrics aren't being found
- New 'API Analytics' tab displays endpoint usage statistics
- Shows total requests, unique endpoints, and most called endpoint
- Table view with top N endpoints (25/50/100/500 configurable)
- Color-coded by usage intensity and endpoint type
- Auto-refreshes every 30 seconds when tab is active
- Includes clear data functionality and helpful documentation
- Uses existing /api/admin/debug/endpoint-usage endpoint
- Replace endpoint racing with round-robin fallback for downloads to reduce CPU usage and prevent cancellation errors
- Fix cache cleanup to use LastWriteTimeUtc instead of unreliable LastAccessTimeUtc
- Add storage mode, cache duration, and download mode to admin config endpoint
- Show actual download path based on storage mode (cache/Music vs downloads)
- Remove complex Item object from playback start (was causing 400 errors)
- Send minimal playback info with just ghost UUID and playback state
- Progress reports already working (204 responses)
- Jellyfin will track session without full item details
- New EndpointBenchmarkService pings all endpoints on startup
- Measures average response time and success rate
- Reorders endpoints by performance (fastest first)
- RoundRobinFallbackHelper now uses benchmarked order
- Racing still happens, but starts with fastest endpoints
- Reduces latency by prioritizing known-fast servers
- Logs benchmark results for visibility
- GetTrackDownloadInfoAsync now uses RaceAllEndpointsAsync instead of TryWithFallbackAsync
- Prevents sequential fallback through all 10 endpoints on cancellation
- Eliminates cascade of 'task was canceled' warnings
- Consistent racing strategy across all download operations
- Respect SquidWTF/Tidal's native search ranking (better than fuzzy matching)
- Interleave local and external results based on average match quality
- Put better-matching source first, preserve original order within each source
- Remove unnecessary re-scoring that was disrupting optimal search results
- Simplifies search logic and improves result relevance
- Race all proxy endpoints in parallel for downloads (SquidWTF)
- Use fastest responding server, cancel slower ones
- Apply same racing strategy to search operations
- Reduces download wait times from 5-10s to sub-second
- Reduces search latency from ~1s to ~300-500ms
- Add RaceAllEndpointsAsync method to RoundRobinFallbackHelper
- Created RoundRobinFallbackHelper for SquidWTF services (eliminates 3 duplicates)
- Moved QueueRequestAsync to BaseDownloadService (eliminates 2 duplicates)
- Moved CalculateArtistMatchScore to FuzzyMatcher (eliminates 2 duplicates)
- Updated all SquidWTF services to use RoundRobinFallbackHelper
- Updated DeezerDownloadService and SquidWTFDownloadService to use base class rate limiting
- Updated SpotifyTrackMatchingService and JellyfinController to use FuzzyMatcher helper
- All 225 tests passing
- Created OdesliService to convert Tidal track IDs to Spotify IDs
- Integrated Odesli API calls into SquidWTF download workflow
- Updated SquidWTFDownloadService to use OdesliService for track metadata enrichment
- Fixed dependency injection in Program.cs for OdesliService
- All 225 tests passing
- Use linksByPlatform.spotify.url from Odesli response
- Extract track ID from Spotify URL using regex
- More reliable than parsing entityUniqueId format
- Matches the approach used in ConvertToSpotifyIdViaOdesliAsync
- Odesli returns entityUniqueId as 'SPOTIFY_SONG::trackid'
- Now extracts just the track ID part after '::'
- Fixes Spotify lyrics not working due to invalid ID format
- Spotify lyrics service expects clean track IDs like '0PgYPBGqF6Wm5KFHQ81nq5'
- Cache mode now uses cache/Music/ (survives restarts, cleaned after 24h)
- Permanent mode uses downloads/ (keeps forever)
- Fixed all three download services: SquidWTF, Deezer, Qobuz
- Files no longer stored in /tmp/allstarr-cache/ which gets wiped on restart
- Both folders are in project root alongside cache/ and downloads/ directories
- 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
- Add GET /api/admin/lyrics/spotify/test endpoint
- Accepts trackId query parameter (Spotify track ID)
- Returns lyrics in both JSON and LRC format
- Useful for testing Spotify lyrics API integration
- Handle nullable duration in LRCLib API responses
- Validate input parameters before making LRCLib requests
- Change SquidWTF artist warning to debug level (expected behavior)
- Prevent JSON deserialization errors when duration is null
- Prevent 400 Bad Request errors from empty track names
- Remove duplicate _httpClient field (use inherited one)
- Replace ValidationResult.Warning with ValidationResult.Failure
- Use PARTIAL status for partial failures
- Revert album endpoint back to ?id= (correct parameter)
- Update SquidWTF validator to test '22' by Taylor Swift
- Create LyricsStartupValidator testing all lyrics services:
* LRCLib API
* Spotify Lyrics Sidecar (docker container)
* Spotify API configuration
- Test song: '22' by Taylor Swift (Spotify ID: 3yII7UwgLF6K5zW3xad3MP)
- Register lyrics validator in startup orchestrator
- Change album endpoint from ?id= to ?f= to match API spec
- Album search parsing is correct (data.albums.items)
- Album detail parsing is correct (data with items array)
- Add TryGetProperty check for artist field in albums response
- Log response keys when artist data not found for debugging
- Improves error handling when API returns albums without artist field
- Add spotify-lyrics-api sidecar container to docker-compose
- Replace direct Spotify API lyrics code with sidecar API calls
- Update SpotifyLyricsService to use sidecar exclusively
- Add LyricsApiUrl setting to SpotifyApiSettings
- Update prefetch to try Spotify lyrics first, then LRCLib
- Remove unused direct API authentication and parsing code
- Check for embedded lyrics in local Jellyfin tracks before fetching from LRCLib
- Remove previously cached LRCLib lyrics when local lyrics are found
- Prevents unnecessary API calls and respects user's embedded lyrics
- Tracks with local lyrics are counted as 'cached' in prefetch stats
- Add complete lyrics ID mapping system with Redis cache, file persistence, and cache warming
- Manual lyrics mappings checked FIRST before automatic search in LrclibService
- Add lyrics status badge to track view (blue badge shows when lyrics are cached)
- Enhance search links to show 'Search: Track Title - Artist Name'
- Fix Active Playlists tab to read from .env file directly (shows all 18 playlists now)
- Add Map Lyrics ID button to every track with modal for entering lrclib.net IDs
- Add POST /api/admin/lyrics/map and GET /api/admin/lyrics/mappings endpoints
- Lyrics mappings stored in /app/cache/lyrics_mappings.json with no expiration
- Cache warming loads lyrics mappings on startup
- All mappings follow same pattern as track mappings (Redis + file + warming)
New LyricsPrefetchService automatically fetches lyrics for all tracks in
Spotify injected playlists. Lyrics are cached in Redis and persisted to
disk for fast loading on startup.
Features:
- Prefetches lyrics for all playlist tracks on startup (after 3min delay)
- Daily refresh to catch new tracks
- File cache at /app/cache/lyrics for persistence
- Cache warming on startup loads lyrics from disk into Redis
- Rate limited to 2 requests/second to be respectful to LRCLIB API
- Logs fetched/cached/missing counts per playlist
Benefits:
- Instant lyrics availability for playlist tracks
- Survives container restarts
- Reduces API calls during playback
- Better user experience with pre-loaded lyrics
Manual external mappings now fetch complete track metadata from the
provider (SquidWTF) instead of using minimal Spotify metadata. This
ensures proper IDs, artist IDs, album IDs, cover art, and all metadata
needed for playback.
Also fixed admin UI to properly detect manual external mappings so
tracks show as 'External' instead of 'Missing'.
Changes:
- Fetch full Song metadata using GetSongAsync when manual mapping exists
- Fallback to minimal metadata if fetch fails
- Admin controller now checks isManualMapping flag to set correct status
- Tracks with manual external mappings now show proper provider badge
Two critical fixes:
1. External songs from manual mappings now get proper IDs (ext-{provider}-song-{id})
- Previously had no ID, causing 'dummy' errors in Jellyfin
- Now follows same format as auto-matched external tracks
2. Admin UI now correctly shows manual external mappings as available
- Previously showed as 'Missing' even after mapping
- Now properly detects manual external mappings and shows provider badge
This fixes the 400 Bad Request errors when trying to play manually mapped tracks.
- Manual mappings now show a blue 'Manual' badge next to the track status
- Added search button (🔍) for missing tracks to help find them
- Backend now returns isManualMapping, manualMappingType, and manualMappingId
- Frontend displays manual mapping indicators for both local and external tracks
- Missing tracks now show a search link to help locate them on SquidWTF
The bug was in PreBuildPlaylistItemsCacheAsync - when a manual external
mapping was found, it was added to matchedTracks but the code used
'continue' to skip to the next track WITHOUT adding it to finalItems.
This meant external manual mappings were never included in the playlist
cache that gets served to clients.
The fix converts the external song to Jellyfin item format and adds it
to finalItems before continuing, ensuring manual external mappings are
properly included in the pre-built playlist cache.
- Fixed CS0136 error: renamed 'doc' to 'extDoc' in AdminController to avoid variable name conflict
- Changed all Redis cache logs (HIT/MISS/SET) to DEBUG level instead of suppressing
- This allows cache logs to be visible in docker logs but not as noisy at INFO level
- Fixed RuntimeBinderException when processing external mappings by replacing dynamic with JsonDocument parsing
- Suppressed cache MISS logs for manual/external mappings (they're expected to be missing most of the time)
- Only log manual/external mapping HITs at INFO level, other cache operations at DEBUG level
- Applied fix to SpotifyTrackMatchingService (2 locations) and AdminController (2 locations)
- Changed parameter type from dynamic? to object? to avoid runtime binding issues
- Added check for JsonValueKind.Undefined in addition to Null
- Fixes crash when checking external mappings that return JsonElement
- Applied fix to both AdminController and SpotifyTrackMatchingService
- Manual mappings now have NO expiration (permanent in Redis)
- Save manual mappings to /app/cache/mappings/*.json files
- Load manual mappings on startup via CacheWarmingService
- Manual mappings are first-order and survive restarts/cache clears
- User decisions are now truly permanent
- When mapping a track to local or external, delete both Redis and file caches
- This forces the matched tracks cache to rebuild with the new mapping
- Ensures manual mappings are immediately reflected in playlists
- Added 5-minute cooldown between matching runs to prevent spam
- Improved cache checking to skip unnecessary matching
- Persist matched tracks cache to file for faster restarts
- Cache warming service now loads matched tracks on startup
- Suppress verbose HTTP client logs (LogicalHandler/ClientHandler)
- Only run matching when cache is missing or manual mappings added
- Changed ActiveDownloads from Dictionary to ConcurrentDictionary for thread safety
- Added automatic cleanup of completed downloads after 5 minutes
- Added automatic cleanup of failed downloads after 2 minutes
- This fixes the 929MB -> 10MB memory issue where downloads were never removed from tracking
- Fixed RuntimeBinderException when comparing JsonElement with null
- Added HasValue() helper method for safe dynamic type checking
- Implemented intelligent session cleanup:
* 50 seconds after playback stops (allows song changes)
* 3 minutes of total inactivity (catches crashed clients)
- Added memory stats endpoint: GET /api/admin/memory-stats
- Added sessions monitoring endpoint: GET /api/admin/sessions
- Added GetSessionsInfo() to JellyfinSessionManager for debugging
- Fixed all nullable reference warnings
- Reduced warnings from 10 to 0
- Missing tracks now show both 'Map to Local' and 'Map to External' buttons
- External tracks continue to show only 'Map to Local' button
- Added openExternalMap() function that opens modal in external mapping mode
- Added event listeners for .map-external-btn buttons
- External mapping button styled with warning color to distinguish from local mapping
- Users can now easily choose between mapping to Jellyfin tracks or external provider IDs
FIXES:
- Fixed track display logic to properly distinguish between external matched and missing tracks
- Missing tracks now show 'Missing' instead of incorrectly showing provider name
- Added support for manual external provider mappings (e.g., SquidWTF IDs)
CHANGES:
- Extended ManualMappingRequest to support ExternalProvider + ExternalId
- Updated SaveManualMapping to handle both Jellyfin and external mappings
- Updated SpotifyTrackMatchingService to check for external manual mappings
- Updated AdminController track details to use proper missing/matched logic
NOTE: Build currently has syntax errors that need to be fixed, but core logic is implemented.
- Fixed SpotifyTrackMatchingService to use GetJsonAsyncInternal for authenticated requests
- This resolves 401 Unauthorized errors when fetching existing playlist tracks
- Should prevent unnecessary rematching when cache is warm
- Fixes 'No Items found in Jellyfin playlist response' warnings
The service was using GetJsonAsync with null headers, causing 401 errors.
Now uses server API key authentication for internal operations.
- Added GetJsonAsyncInternal method to JellyfinProxyService for server-side requests
- Uses server API key instead of client tokens for internal operations
- Updated UpdateSpotifyPlaylistCounts to use internal method with proper authentication
- This should resolve 401 Unauthorized errors when updating playlist counts
Now Spotify playlists should show correct track counts without authentication issues.
- Added UserId parameter to playlist items request to avoid 401 Unauthorized
- Fixed JsonElement casting issue that was causing InvalidCastException
- This should resolve both the authentication error and the track count update
Now Spotify playlists should show correct track counts without authentication errors.
- Changed totalAvailableCount calculation to include both local and external matched tracks
- Updated logging to show breakdown of local vs external tracks
- This fixes Discover Weekly and other external-only playlists showing 0 tracks in clients
Now playlists with all external tracks will show correct track counts in Feishin and other clients.
- Fixed UpdateSpotifyPlaylistCounts to use GetPlaylistByJellyfinId instead of GetPlaylistById
- Fixed GetSpotifyPlaylistTracksOrderedAsync to use correct playlist config lookup
- Added diagnostic logging for playlist config lookups
- Removed test-websocket.html file
This fixes the issue where Spotify playlists showed 0 tracks in playlist lists
but worked correctly when accessed directly.
- PreBuildPlaylistItemsCacheAsync was failing with HTTP 401
- Background services don't have client headers for authentication
- Now manually creates X-Emby-Authorization header with API key
- Fixes 'Failed to fetch Jellyfin playlist items: HTTP 401' warning
- Playlist items cache now builds successfully after track matching
All 225 tests pass.
- Fix playlist counting logic to use fuzzy matching (same as track view)
- Count local tracks by matching Jellyfin tracks to Spotify tracks
- Count external matched tracks from cache
- Count missing tracks (not found locally or externally)
- Progress bars now show three colors:
* Green: Local tracks in Jellyfin
* Orange: External matched tracks (SquidWTF/Deezer/Qobuz)
* Grey: Missing tracks (not found anywhere)
- Add 'Missing' badge to tracks that couldn't be found
- Missing tracks can still be manually mapped
- Fixes incorrect counts like '28 matched • 1 missing' showing 29 external tracks
All 225 tests pass.
- Backend now returns mapped track details after saving
- Frontend updates track in-place without requiring page refresh
- Track status changes from External to Local immediately
- Map button is removed after successful mapping
- Playlist counts refresh in background
- Improved UX: no more 'refresh the playlist' message
All 225 tests pass.
- Fix manual mapping track selection visual feedback (use accent color + background)
- Clear all playlist caches after manual mapping (matched, ordered, items)
- Strip [S] suffix from titles/artists/albums when searching for lyrics
- Add [S] suffix to artist and album names when song has [S] for consistency
- Ensures external tracks are clearly marked across all metadata fields
All 225 tests pass.
- Pre-build playlist items cache during track matching for instant serving
- Add PreBuildPlaylistItemsCacheAsync() to SpotifyTrackMatchingService
- Combines local Jellyfin tracks + external matched tracks in correct Spotify order
- Saves to both Redis and file cache for persistence across restarts
- Change matching interval from hardcoded 30 minutes to configurable (default: 24 hours)
- Add SPOTIFY_IMPORT_MATCHING_INTERVAL_HOURS environment variable
- Set to 0 to only run once on startup (manual trigger still works)
- Add endpoint usage files to .gitignore
- Update documentation in README and .env.example
Rationale: Spotify playlists like Discover Weekly update once per week,
so running every 24 hours is more than sufficient. Pre-building the cache
eliminates slow 'on the fly' playlist building.
All 225 tests pass.
- External tracks are injected on-the-fly, not stored in Jellyfin DB
- Check spotify:matched:ordered cache to get accurate external count
- Calculate external tracks as: total matched - local tracks
- This will properly show the two-color progress bar (green local + orange external)
- All 225 tests passing
- Made search query clickable - opens provider-specific search
- Added searchProvider() function with URLs for Deezer, Qobuz, SquidWTF
- Enhanced console logging to debug progress bar data
- Logs raw playlist data and calculated percentages
- Search link opens in new tab
- Increased progress bar height from 8px to 12px for better visibility
- Changed colors to more vibrant shades (green #10b981, orange #f59e0b)
- Added console debug logging for playlist stats
- Shows local (green) and external (orange) track percentages side-by-side
- Fixed external track detection (check for provider prefix in ID)
- Added genre support to MusicBrainz service (inc=genres+tags)
- Created GenreEnrichmentService for async genre lookup with caching
- Show provider name and search query for external tracks in admin UI
- Display search query that will be used for external track streaming
- Aggregate playlist genres from track genres
- All 225 tests passing
- Fixed AdminController export/import .env endpoints (moved from ConfigUpdateRequest class)
- Added ArtistId and AlbumId to integration test fixtures
- All 225 tests passing
- Version set to 1.0.0 (semantic versioning)
- MusicBrainz service ready for future ISRC-based matching (1.1.0)
- Import/export handles full .env configuration with timestamped backups
- Added MusicBrainzSettings model with username/password authentication
- Created MusicBrainzService with ISRC lookup and recording search
- Implements proper rate limiting (1 req/sec) per MusicBrainz rules
- Added meaningful User-Agent header as required
- Registered service in Program.cs with configuration
- Added MusicBrainz section to appsettings.json
- Credentials stored in .env (MUSICBRAINZ_USERNAME/PASSWORD)
Next: Add to admin UI and implement import/export for .env
Verified against real Jellyfin responses for tracks, albums, artists, and playlists:
- Reordered fields to match Jellyfin's exact field order
- Added missing fields: PremiereDate, HasLyrics, Container, ETag, etc.
- Fixed MediaType to 'Unknown' for albums/artists (not null)
- Fixed UserData.Key format to match Jellyfin patterns
- Added ParentLogoItemId, ParentBackdropItemId for proper hierarchy
- Fixed Genres/GenreItems to always be arrays (never null)
- Added complete MediaStream structure with all Jellyfin fields
- Playlists now have MediaType='Audio' to match real playlists
- All responses now perfectly mimic real Jellyfin structure
- Fixed UpdateSpotifyPlaylistCounts to properly handle file cache without skipping items
- Added Genres and GenreItems fields to all tracks (empty array if no genre)
- Added complete MediaStreams with audio codec info for external tracks
- Added missing MediaSource fields: IgnoreDts, IgnoreIndex, GenPtsInput, HasSegments
- Ensured Artists array never contains null values
- All external tracks now have proper genre arrays to match Jellyfin structure
- Added MediaStreams, MediaAttachments, Formats as empty arrays instead of null
- Added RunTimeTicks field to MediaSources
- Added detailed logging to GetExternalChildItems to debug artist album issues
- This should fix 'type Null is not a subtype of type List<dynamic>' error
- Fixed LrclibService.GetLyricsAsync call to use empty string and 0 for duration
- Fixed nullability warnings in SpotifyTrackMatchingService by explicitly casting to nullable tuple
- Blue segment shows local tracks percentage
- Yellow segment shows external matched tracks percentage
- Bar fills to 100% when all tracks are matched
- Added tooltips showing track counts on hover
- Added all required boolean fields to MediaSources (IsRemote, IsInfiniteStream, RequiresOpening, etc)
- UpdateSpotifyPlaylistCounts now loads from file cache if Redis is empty
- This fixes 'type Null is not a subtype of type bool' error in Finamp
- Playlist counts now show correctly even after container restart
- Created ParallelMetadataService to race all providers and return fastest result
- Search now uses parallel service when available for lower latency
- Pre-fetch LRCLib lyrics for top 3 search results in background
- FuzzyMatcher already handles apostrophe normalization (applied everywhere)
- Enhanced FuzzyMatcher to normalize apostrophes (', ', ', etc) for better matching
- Added Redis-only caching for search results (15 min TTL)
- Added pattern-based cache deletion for search and image keys
- Added URL input field in Map to Local modal to paste Jellyfin track URLs
- Added /api/admin/jellyfin/track/{id} endpoint to fetch track details by ID
- Fixed duplicate cache key declaration in GetSpotifyPlaylistTracksOrderedAsync
- Updated cache clearing to include new spotify:playlist:items:* keys
- Properly parse and modify Fields parameter instead of appending to end
- Fixes 400 BadRequest errors from malformed URLs
- Now correctly adds MediaSources to Fields parameter value
CRITICAL FIX: Don't convert Jellyfin items to Song objects and back!
- Get raw Jellyfin playlist items with MediaSources field
- Reorder them according to Spotify positions
- Inject external tracks where needed
- Return raw items preserving ALL Jellyfin metadata (bitrate, etc)
This ensures local tracks in Spotify playlists show correct bitrate
just like regular Jellyfin playlists.
- Avoid JavaScript escaping issues with inline onclick handlers
- Use data attributes to store track info
- Add event listeners after DOM is created
- Prevents syntax errors with special characters in track names
- Ensure MediaSources field is included when proxying browse requests
- Applies to /Users/{userId}/Items (favorites, recently played, etc)
- Jellyfin doesn't include MediaSources by default, must be requested
- Now bitrate info shows correctly in all browse contexts
- Changed ChildCount to reflect tracks actually in Jellyfin (local + external matched)
- Previously was incorrectly adding missing tracks to the count
- Now clients see the correct number of playable tracks
- Uses spotify:matched:ordered cache to count external matched tracks
Backend changes:
- Distinguish between local tracks (in Jellyfin library) and external tracks (downloaded)
- Track external matched vs external missing counts
- Calculate completion percentage for each playlist
Frontend changes:
- Show detailed breakdown: X local • Y matched • Z missing
- Display completion percentage with progress bar
- Color-coded stats (green=local, blue=matched, yellow=missing)
- Updated table headers for clarity
- Added clearing of spotify:matched:* and spotify:matched:ordered:* keys
- This ensures bitrate metadata fix takes effect after cache clear
- Returns count of cleared Redis keys in response
- Add 'Map to Local' button for external tracks in playlist viewer
- Search Jellyfin library to find local tracks
- Save manual mappings (Spotify ID → Jellyfin ID) in cache
- Manual mappings take priority over fuzzy matching
- Clear playlist cache when mapping is saved to force refresh
- UI shows which tracks are manually mapped in logs
CRITICAL FIX: Changed matching strategy completely
- Step 1: Match ALL Jellyfin tracks to Spotify positions (fuzzy 70%)
- Step 2: Build playlist in Spotify order using matched Jellyfin tracks
- Step 3: Fill remaining gaps with external tracks (cached or on-demand)
- Step 4: Add any unmatched Jellyfin tracks at the end
This ensures Jellyfin tracks are ALWAYS used when they match, preventing
external tracks from being used when local versions exist.
- Search external providers in real-time if no cached match exists
- Use fuzzy matching (60% threshold) for external tracks
- Ensures external tracks are always available even without pre-matching
- Local tracks still prioritized first (70% threshold)
- Remove Spotify ID/ISRC matching (Jellyfin plugin doesn't add these)
- Use ONLY fuzzy name matching (title + artist, 70% threshold)
- LOCAL tracks ALWAYS used first before external providers
- Include ALL tracks from Jellyfin playlist (even if not in Spotify)
- Prevent duplicate track usage with HashSet tracking
- AdminController also updated to match by name for Local/External badges
- Better logging with emojis for debugging
- Add fuzzy matching by title+artist as fallback (like Jellyfin Spotify Import plugin)
- Add clear error messages when JELLYFIN_USER_ID is not configured
- Add emoji logging for easier debugging (🔍📌✅❌)
- Check HTTP status code when fetching playlist items
- This should fix the issue where all tracks show [S] even when they exist locally
- CRITICAL FIX: Add UserId parameter to all Jellyfin playlist item fetches (fixes 400 BadRequest errors)
- Fix GetPlaylists to correctly count local/missing tracks
- Fix GetSpotifyPlaylistTracksOrderedAsync to find local tracks (was serving external tracks for everything)
- Fix SpotifyTrackMatchingService to skip tracks already in Jellyfin
- Add detailed debug logging for track matching (LOCAL by ISRC/Spotify ID, EXTERNAL match, NO MATCH)
- Add 'Match Tracks' button for individual playlists (not all playlists)
- Add 'Match All Tracks' button for matching all playlists at once
- Add JELLYFIN_USER_ID to web UI configuration tab for easy setup
- Add /api/admin/playlists/match-all endpoint
This fixes the issue where local tracks weren't being used - the system was downloading
from SquidWTF even when files existed locally in Jellyfin.
- Register as singleton first, then as hosted service
- Allows AdminController to inject and trigger matching manually
- Added better logging for Jellyfin playlist fetch failures
- Logs when JellyfinId is missing or API calls fail
- Helps debug '0 local / 0 missing' issue
- Fetches current Jellyfin playlist track count
- Compares with Spotify playlist total to calculate missing tracks
- Shows: '25 local / 5 missing' instead of just track count
- Local = tracks currently in Jellyfin playlist
- Missing = Spotify tracks not yet in Jellyfin (need to be matched)
- Helps users see which playlists need track matching
- Added 'Match Tracks' button to trigger matching for specific playlist
- Added Local/External column (shows '-' for now, will populate after matching)
- New endpoint: POST /api/admin/playlists/{name}/match
- Injects SpotifyTrackMatchingService into AdminController
- UI shows: Name | Spotify ID | Total | Local/External | Cache Age | Actions
- Allows users to manually trigger matching without waiting 30 minutes
- Local Jellyfin tracks don't have Spotify IDs (only plugin-added tracks do)
- Now matches by ISRC first (most reliable), then falls back to Spotify ID
- Builds dictionaries for fast lookup: existingBySpotifyId and existingByIsrc
- Prioritizes local tracks over external matches
- Logs: 'X tracks (Y with Spotify IDs, Z with ISRCs)'
- This fixes all tracks showing [S] - now uses local files when available
- After linking/unlinking, update UI state directly instead of refetching
- Avoids 7+ second delay from fetching track stats for all playlists
- Added data-playlist-id attribute to table rows for easy lookup
- Only refreshes Active Playlists tab (fast operation)
- Much better UX when linking multiple playlists
- Include JellyfinId when serializing playlists after removal
- Ensures config format stays consistent: [Name,SpotifyId,JellyfinId,position]
- Fixes issue where removed playlists would stay in the list
- Fetch existing tracks from Jellyfin playlist before matching
- Extract Spotify IDs from ProviderIds to identify already-matched tracks
- Only search for tracks not already in Jellyfin
- Logs: 'Matching X/Y tracks (skipping Z already in Jellyfin)'
- Significantly reduces matching time for playlists with local content
- Example: If 20/50 tracks exist locally, only searches for 30 tracks
- Process tracks in batches of 11 (matches SquidWTF provider count)
- Each batch runs 11 parallel searches (one per provider)
- Wait 150ms between batches (not between individual tracks)
- This is ~11x faster: 50 tracks now takes ~1 second instead of ~7.5 seconds
- Round-robin in SquidWTF ensures each parallel request hits a different provider
- Maintains rate limiting while maximizing throughput
- Added JellyfinId field to SpotifyPlaylistConfig model
- Updated config format: [[Name,SpotifyId,JellyfinId,position],...]
- LinkPlaylist now stores both Jellyfin and Spotify playlist IDs
- IsSpotifyPlaylist() now checks by Jellyfin playlist ID (not Spotify ID)
- GetJellyfinPlaylists shows linked status by checking JellyfinId
- Updated Program.cs to parse new 4-field format
- Backward compatible: JellyfinId defaults to empty string for old configs
This fixes the issue where playlists weren't being recognized as configured
because the code was checking Jellyfin playlist IDs against Spotify IDs.
- Replace REST API calls with GraphQL API (api-partner.spotify.com/pathfinder/v1/query)
- Use same persisted query approach as Jellyfin Spotify Import plugin
- GraphQL API is more reliable and has better rate limits
- Fetch playlists with operationName: fetchPlaylist
- Parse GraphQL response structure (playlistV2, itemV2, etc)
- This should eliminate TooManyRequests errors
- Increase delay between playlist fetches from 1s to 3s
- Only delay between playlists, not after the last one
- Add debug logging for rate limit delays
- Spotify is very aggressive with rate limiting on their API
- Add UserId parameter to playlist items API call (required by Jellyfin)
- Auto-fetch first user if no user ID configured
- Simplify Fields parameter to only request Path
- This fixes the 400 BadRequest errors when loading Jellyfin playlists tab
- Read playlists from .env file instead of stale in-memory config
- This fixes issue where linking multiple playlists only saved the last one
- Add prominent restart banner at top when config changes
- Update UI immediately after linking/unlinking playlists
- Banner shows 'Restart Now' button and dismissible option
- All config changes now show the restart banner instead of toast messages
- GetJellyfinPlaylists now reads from .env for accurate linked status
- Add cookieDateInitialized flag to prevent infinite init-cookie-date calls
- Auto-initialize cookie date when cookie exists but date not tracked
- Improve local vs external track detection in Jellyfin playlists
- Support multiple Spotify playlist ID formats (ID, URI, URL)
- Fix pragma warnings in legacy playlist parsing code
- Simplify GetPlaylistTrackStats to check Path property for local tracks
- Add SPOTIFY_API_SESSION_COOKIE_SET_DATE to docker-compose.yml env mapping
- Mount .env file in container for web UI to update
- Add SessionCookieSetDate loading in Program.cs
- Improve .env update logic with better error handling and logging
- Auto-initialize cookie date when cookie exists but date not set
- Simplify local vs external track detection in Jellyfin playlists
- Enhanced Spotify playlist ID parsing (supports ID, URI, and URL formats)
- Better UI clarity: renamed tabs to 'Link Playlists' and 'Active Playlists'
- GetStatus() no longer calls Spotify API at all
- Status is now determined purely from configuration
- Fixes rate limiting issue (429 errors every 30 seconds)
- Spotify API should only be called when actually fetching playlists
- Shows overlay with spinner during container restart
- Displays status messages: stopping, waiting, reloading
- Shows elapsed time while waiting for server
- Extended timeout to 60 seconds
- Hides overlay and shows toast if timeout exceeded
- Added POST /api/admin/restart endpoint using Docker socket
- Mounts Docker socket read-only in docker-compose.yml
- Admin UI now has working 'Restart Container' button
- Auto-reloads page after container comes back up
- Falls back to manual restart instructions if socket unavailable
- Added GET /api/admin/jellyfin/playlists to fetch all playlists from Jellyfin
- Added POST /api/admin/jellyfin/playlists/{id}/link to link playlist to Spotify
- Added DELETE /api/admin/jellyfin/playlists/{name}/unlink to remove link
- Added new 'Jellyfin Playlists' tab in admin UI showing all playlists
- Shows link status for each playlist (Linked/Not Linked)
- Link modal accepts Spotify playlist ID or full URL
- Renamed 'Playlists' tab to 'Configured Playlists' for clarity
- Removed LocalTracksPosition from playlist table (uses Spotify order now)
- Removed LocalTracksPosition from add playlist modal and JS
- Added /api/admin/config/init-cookie-date endpoint to auto-set date
- Cookie date auto-initializes when cookie exists but date is unknown
- Improved user display message when profile scope unavailable
- TOTP tokens work for playlists but don't have user-read-private scope
- Updated album suffix from ' - S' to ' [S]'
- Updated artist suffix from ' - S' to ' [S]'
- Added song title suffix ' [S]' for external/streaming tracks
- Cleaner bracket format is more visible and consistent
- Fix auth status detection to use token validity instead of /me endpoint
- Add SessionCookieSetDate to SpotifyApiSettings for tracking cookie age
- Auto-set cookie date when updating sp_dc via admin UI
- Add edit buttons for all config settings (Spotify, Deezer, Qobuz, SquidWTF, Jellyfin)
- Show cookie age with color-coded expiration warnings (green/yellow/red)
- Display cookie age on both Dashboard and Config tabs
- Add generic edit setting modal supporting text/password/number/toggle/select inputs
- Remove SquidWTF base URL (not configurable)
- Add restart container button with manual restart instructions
- Admin API and static files only accessible on port 5275
- Main proxy port (8080) no longer serves admin endpoints
- AdminPortFilter rejects admin requests on wrong port
- AdminStaticFilesMiddleware only serves static files on admin port
- Port 5275 NOT exposed in Dockerfile or docker-compose by default
- Access admin UI via SSH tunnel or by uncommenting port mapping
- Add AdminController with API endpoints for status, playlists, config
- Add web UI dashboard at /admin with dark theme
- Disable SpotifyMissingTracksFetcher when SpotifyApi is enabled with cookie
- Support viewing playlists, tracks, and managing configuration
- Add .env file modification from UI (requires restart to apply)
- Dashboard auto-refreshes status every 30 seconds
- Use pre-scraped TOTP secrets from Viperinius/spotify-totp-secrets
- Generate TOTP code using server time and cipher transformation
- Add OTP.NET package for TOTP generation
- Match the authentication flow used by jellyfin-plugin-spotify-import
- Changed routine WebSocket messages (Sessions, KeepAlive) to debug level
- Reduced keep-alive spam - 401 on expired tokens is expected, not an error
- Added periodic progress logging for local tracks (every 10 seconds)
- Log warnings only when progress reports fail
Features:
- SpotifyApiClient: Direct Spotify API client using sp_dc session cookie
- SpotifyLyricsService: Fetch synced lyrics from Spotify's color-lyrics API
- SpotifyPlaylistFetcher: Get playlists with correct track ordering and ISRC codes
- SpotifyTrackMatchingService: ISRC-based exact track matching for external providers
Improvements:
- Lyrics endpoint now prioritizes: 1) Jellyfin embedded, 2) Spotify synced, 3) LRCLIB
- Fixed playback progress reporting - removed incorrect body wrapping for Jellyfin API
- Added SpotifyApiSettings configuration model
Security:
- Session cookie and client ID properly masked in startup logs
- All credentials read from environment variables only
Local tracks in Jellyfin playlist are now returned first, with
matched external tracks (from squid.wtf) appended at the end.
Previously the code tried to match local tracks by exact title/artist
which often failed due to naming differences.
- Refactor proxy service methods to return (Body, StatusCode) tuples
- Add HandleProxyResponse helper for consistent status code handling
- Fix 401 authentication errors being returned as 204
- Add explicit session reporting endpoints for playback tracking
- Ensure local track playback sessions are forwarded to Jellyfin
- All 219 tests passing
- Add /UserFavoriteItems/{itemId} route (official Jellyfin API)
- Keep /Users/{userId}/FavoriteItems/{itemId} for compatibility
- Support userId in query string or path
- Add route logging to debug which endpoint is being called
- Fix unsynced lyrics showing all at 0:00 by omitting Start field
- Add DeleteAsync to proxy service for proper auth forwarding
- Fix favorite/unfavorite endpoints to use proxy service
- Add comprehensive logging for debugging
- Search API is more forgiving than exact get endpoint
- Scores results by track/artist similarity and duration match
- +20 point bonus for results with synced lyrics
- Falls back to exact match if search fails
- Improves lyrics hit rate for metadata variations
The playlist list was going through SearchItems (not ProxyRequest), so
UpdateSpotifyPlaylistCounts was never called. Now updates counts in both:
- SearchItems when browsing playlists (no search term)
- ProxyRequest for other playlist list requests
This fixes playlists showing 0 tracks when they should show the count
of missing tracks available.
SECURITY FIX: Stop using server API key when clients don't provide auth
Before: If client sent no auth → proxy used server API key → gave them access
After: If client sends no auth → proxy sends no auth → Jellyfin rejects (401)
This ensures:
- Unauthenticated users can't piggyback on server credentials
- All actions are properly attributed to the actual user
- Jellyfin's auth system works as intended
- Server API key only used for internal operations (images, library detection)
Updated test to reflect new behavior: GetJsonAsync without client headers
should NOT add any authentication.
- Log when checking items for Spotify playlists
- Log cache lookups and file cache fallbacks
- Log successful ChildCount updates
- Log when no Spotify playlists found
- Helps diagnose why playlist counts might not be updating correctly
- Log all proxied endpoints to /app/cache/endpoint-usage/endpoints.csv
- CSV format: timestamp, method, path, query string
- Add GET /debug/endpoint-usage?api_key=KEY to view statistics
- Shows top N endpoints by usage count
- Filter by date with since parameter
- Returns total requests, unique endpoints, first/last seen
- Add DELETE /debug/endpoint-usage?api_key=KEY to clear logs
- Thread-safe file appending
- Helps identify which endpoints clients actually use
- Can inform future blocklist/allowlist decisions
- Intercept playlist list responses and update ChildCount for Spotify playlists
- Shows the number of missing tracks found (local + matched external)
- Fixes playlist showing 0 songs when Jellyfin has no local files
- Reads from cache (Redis or file) to get accurate count
- Search forward 12 hours from sync time
- Search backward 24 hours from sync time (was 12 hours)
- Ensures yesterday's file is always found when running at 11 AM after 4 PM sync
- Sync runs daily at 4:15 PM, so 24h backward always catches previous day's file
- List all files in cache directory on startup
- Show expected file paths and actual file existence
- Log each step of cache checking process
- Add phase indicators for forward/backward search
- Show when cache exists and fetch is skipped
- Help diagnose why yesterday's cache files aren't being loaded
- On startup, if no cache exists, fetch immediately regardless of sync window
- Regular background checks still respect sync window timing
- Ensures playlists are populated even if app restarts before sync time
- When favoriting an external track, automatically copy to /kept folder
- Organized as Artist/Album/Track structure
- Includes cover art if available
- Downloads track first if not already cached
- Add KEPT_PATH and CACHE_PATH volumes to docker-compose
- Update .env.example and README with new feature
- Save missing tracks to /app/cache/spotify/*.json files
- 24-hour TTL for file cache
- Fallback to file cache when Redis is empty/restarted
- Controller checks file cache before returning empty playlists
- Ensures playlist data persists across Redis restarts
- Check external playlists first (fast check)
- Only check Spotify if enabled (avoid extra API call)
- Fall back to regular Jellyfin playlists
- Maintains correct behavior for all playlist types
- Remove duplicate JellyfinUrl and ApiKey from SpotifyImportSettings
- Use existing JELLYFIN_URL and JELLYFIN_API_KEY settings
- Simplify configuration - no duplicate settings needed
- Update documentation and .env.example
- Check playlist name instead of creating virtual playlists
- Intercept playlist items request for Spotify playlists
- Fill empty playlists with matched tracks from providers
- Works with existing Jellyfin Spotify Import plugin playlists
Jellyfin sends ETag, Last-Modified, and Cache-Control headers that allow clients like Feishin to cache songs locally. Proxy now forwards these headers so clients don't re-download songs unnecessarily.
- Move RegisterDownloadedSongAsync call before lock release
- Prevents race condition where multiple threads download same song
- Second thread now finds registered mapping before starting download
- Fixes duplicate files like 'Song (1).flac', 'Song (2).flac'
- Library scan and album download still happen after lock release
- Decode 6 base64 URLs at startup (1 primary + 5 backups)
- Automatic fallback when endpoint fails
- All services try next endpoint on failure
- Metadata, Download, and Validator all support fallback
- Endpoints: triton.squid.wtf, wolf/hund/maus/vogel/katze.qqdl.site
- Logs which endpoint is being used
- Cycles through all endpoints before giving up
- Decode base64 URL once at startup in Program.cs
- Pass decoded URL as constructor parameter to all SquidWTF services
- Services receive plain URL, no decoding logic in service classes
- Cleaner separation: encoding/decoding only in Program.cs
- All service code remains unchanged, just receives decoded URL
- Changed from decoding in constructor to static readonly fields
- Decode happens once per class initialization, stored in memory
- Cleaner implementation, no repeated decoding per instance
- All three SquidWTF services updated (Metadata, Download, Validator)
Recently added, recently played, and frequently played sections use
different query parameters (SortBy, SortOrder, Filters, etc) that we
weren't forwarding. Now we pass the complete query string to Jellyfin.
- Create LrclibService to fetch lyrics from lrclib.net API
- Add LyricsInfo model for lyrics data
- Add /Audio/{itemId}/Lyrics and /Items/{itemId}/Lyrics endpoints
- Support both local and external songs
- Cache lyrics for 30 days in Redis
- Return lyrics in Jellyfin format with synced/plain lyrics
- Always send body content for POST requests, even if empty (send '{}')
- Update TODO.md to mark tasks 2 and 3 as done
- Improve logging for POST body debugging
@@ -74,8 +76,6 @@ There's an environment variable to modify this.
**Recommended workflow**: Use the `sp_dc` cookie method alongside the [Spotify Import Plugin](https://github.com/Viperinius/jellyfin-plugin-spotify-import?tab=readme-ov-file).
**Recommended workflow**: Use the `sp_dc` cookie method alongside the [Spotify Import Plugin](https://github.com/Viperinius/jellyfin-plugin-spotify-import?tab=readme-ov-file).
### Nginx Proxy Setup (Required)
### Nginx Proxy Setup (Required)
This service only exposes ports internally. You can use nginx to proxy to it, however PLEASE take significant precautions before exposing this! Everyone decides their own level of risk, but this is currently untested, potentially dangerous software, with almost unfettered access to your Jellyfin server. My recommendation is use Tailscale or something similar!
This service only exposes ports internally. You can use nginx to proxy to it, however PLEASE take significant precautions before exposing this! Everyone decides their own level of risk, but this is currently untested, potentially dangerous software, with almost unfettered access to your Jellyfin server. My recommendation is use Tailscale or something similar!
@@ -139,8 +139,14 @@ This project brings together all the music streaming providers into one unified
Allstarr automatically fills your Spotify playlists (like Release Radar and Discover Weekly) with tracks from your configured streaming provider (SquidWTF, Deezer, or Qobuz). This works by intercepting playlists created by the Jellyfin Spotify Import plugin and matching missing tracks with your streaming service.
Allstarr automatically fills your Spotify playlists (like Release Radar and Discover Weekly) with tracks from your configured streaming provider (SquidWTF, Deezer, or Qobuz). This works by intercepting playlists created by the Jellyfin Spotify Import plugin and matching missing tracks with your streaming service.
_logger.LogWarning("Spotify rate limit hit (429) when fetching playlist {PlaylistId}. Waiting {Seconds}s before retry...",playlistId,retryAfter.TotalSeconds);
"✓ Cached {Matched}/{Total} tracks for {Playlist} via GREEDY ASSIGNMENT (ISRC: {Isrc}, Fuzzy: {Fuzzy}, No match: {NoMatch}) - manual mappings will be applied next",
"✓ Cached {Matched}/{Total} tracks for {Playlist} via GREEDY ASSIGNMENT (ISRC: {Isrc}, Fuzzy: {Fuzzy}, No match: {NoMatch}) - cache expires in {Hours:F1}h",
<buttononclick="matchAllPlaylists()"title="Match tracks for all playlists against your local library and external providers. This may take several minutes.">Match All Tracks</button>
<buttononclick="matchAllPlaylists()"title="Match tracks for all playlists against your local library and external providers. This may take several minutes.">Match All Tracks</button>
<buttononclick="refreshPlaylists()"title="Fetch the latest playlist data from Spotify without re-matching tracks.">Refresh All</button>
<buttononclick="refreshPlaylists()"title="Fetch the latest playlist data from Spotify without re-matching tracks.">Refresh All</button>
<buttononclick="openEditSetting('CACHE_DURATION_HOURS', 'Cache Duration (hours)', 'number', 'How long to keep cached files before deletion')">Edit</button>
<buttononclick="openEditSetting('SPOTIFY_API_SESSION_COOKIE', 'Spotify Session Cookie', 'password', 'Get from browser dev tools while logged into Spotify. Cookie typically lasts ~1 year.')">Update</button>
<buttononclick="openEditSetting('SPOTIFY_API_SESSION_COOKIE', 'Spotify Session Cookie', 'password', 'Get from browser dev tools while logged into Spotify. Cookie typically lasts ~1 year.')">Update</button>
<buttononclick="openEditSetting('JELLYFIN_USER_ID', 'Jellyfin User ID', 'text', 'Required for playlist operations. Get from Jellyfin user profile URL: userId=...')">Edit</button>
<buttononclick="openEditSetting('JELLYFIN_USER_ID', 'Jellyfin User ID', 'text', 'Required for playlist operations. Get from Jellyfin user profile URL: userId=...')">Edit</button>
<buttononclick="openEditSetting('SPOTIFY_IMPORT_MATCHING_INTERVAL_HOURS', 'Matching Interval (hours)', 'number', 'How often to check for playlist updates')">Edit</button>
Enter the Spotify playlist ID or URL. Allstarr will automatically download missing tracks from your configured music service.
Select a playlist from your Spotify library or enter a playlist ID/URL manually. Allstarr will automatically download missing tracks from your configured music service.
tbody.innerHTML='<tr><td colspan="6" style="text-align:center;color:var(--text-secondary);padding:40px;">No playlists configured. Link playlists from the Jellyfin Playlists tab.</td></tr>';
tbody.innerHTML='<tr><td colspan="7" style="text-align:center;color:var(--text-secondary);padding:40px;">No playlists configured. Link playlists from the Jellyfin Playlists tab.</td></tr>';
}
}
return;
return;
}
}
@@ -1514,10 +1638,16 @@
// Debug logging
// Debug logging
console.log(`Progress bar for ${p.name}: local=${localPct}%, external=${externalPct}%, missing=${missingPct}%, total=${completionPct}%`);
console.log(`Progress bar for ${p.name}: local=${localPct}%, external=${externalPct}%, missing=${missingPct}%, total=${completionPct}%`);
console.error('Failed to fetch tracks:',res.status,res.statusText);
document.getElementById('tracks-list').innerHTML='<p style="text-align:center;color:var(--error);padding:40px;">Failed to load tracks: '+res.status+' '+res.statusText+'</p>';
return;
}
constdata=awaitres.json();
constdata=awaitres.json();
console.log('Tracks data received:',data);
if(!data||!data.tracks){
console.error('Invalid data structure:',data);
document.getElementById('tracks-list').innerHTML='<p style="text-align:center;color:var(--error);padding:40px;">Invalid data received from server</p>';
# Note: This image only supports AMD64. On ARM64 systems, Docker will use emulation.
spotify-lyrics:
spotify-lyrics:
image:akashrchandran/spotify-lyrics-api:latest
image:akashrchandran/spotify-lyrics-api:latest
platform:linux/amd64
container_name:allstarr-spotify-lyrics
container_name:allstarr-spotify-lyrics
restart:unless-stopped
restart:unless-stopped
ports:
ports:
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.