Commit Graph

551 Commits

Author SHA1 Message Date
670544a9d6 Fix AdminController Spotify 429 rate limiting in Link Playlists tab
- 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
2026-02-09 16:32:18 -05:00
0dca6b792d Fix Spotify 429 rate limiting and startup performance issues
- 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
2026-02-09 16:09:38 -05:00
f135db3f60 fix: use GraphQL for user playlists to avoid 429 rate limits
Some checks failed
CI / build-and-test (push) Has been cancelled
- 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
2026-02-09 15:10:14 -05:00
2b76fa9e6f Enhance README with features and images
Updated README.md to enhance feature descriptions and add images.
2026-02-09 14:54:43 -05:00
6949f8aed4 feat: implement per-playlist cron scheduling with persistent cache
- 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
2026-02-09 14:23:23 -05:00
a37f7e0b1d feat: add sync schedule editing and improve Spotify rate limit handling
Renamed Active Playlists to Injected Playlists. Added sync schedule column with inline edit button. Added endpoint to update playlist sync schedules. Improved error handling for Spotify rate limits with user-friendly messages.
2026-02-09 13:22:02 -05:00
2b4cd35cf7 feat: add per-playlist cron sync schedules
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.
2026-02-09 13:15:04 -05:00
faa07c2791 fix: resolve build errors in forwarded headers and config endpoints
Fixed duplicate builder variable, deprecated KnownNetworks property, and removed non-existent SpotifyImportSettings properties from config endpoint.
2026-02-09 13:12:21 -05:00
bdd753fd02 feat: add admin UI improvements and forwarded headers support
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.
2026-02-09 12:49:50 -05:00
0a07804833 feat: standardize download path configuration and auto-migrate .env
- 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
2026-02-09 12:29:57 -05:00
6c14fc299c fix: prevent duplicate downloads and lock release bug
- 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
2026-02-09 12:26:08 -05:00
b03a4b85c9 fix: cache cleanup service using wrong path
- 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
2026-02-09 12:24:48 -05:00
565cb46b72 feat: add proper multi-artist support with ArtistIds list
- 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
2026-02-09 12:22:40 -05:00
6357b524da remove monochrome-api.samidy.com endpoint
Some checks failed
CI / build-and-test (push) Has been cancelled
Free tier account can't stream or download, only useful for metadata fallback.
2026-02-08 01:49:12 -05:00
aa9f0d0345 fix: use unified download structure for cache and permanent files
- 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
2026-02-08 01:38:14 -05:00
b0e07404c9 refactor: unified download folder structure
- 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
2026-02-08 01:25:24 -05:00
8dbf37f6a3 refactor: remove sync window logic from Spotify Import
- 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)
2026-02-08 01:21:45 -05:00
baab1e88a5 docs: update README and .env.example with new download structure
- Reorganize downloads into downloads/{permanent,cache,kept}
- Update Spotify Import configuration (keep sync window settings)
- Expand Jellyfin API endpoints documentation (primary focus)
- Move Jellyfin backend section before Subsonic
- Simplify Spotify Import documentation
- Add all manual API trigger endpoints
- Update download folder structure diagram
- Add MIGRATION.md guide for existing installations
2026-02-08 01:17:07 -05:00
972756159d feat: add quick health checks before trying endpoints
- 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
2026-02-08 00:05:27 -05:00
f59f265ad4 feat: increase SquidWTF download service timeout to 5 minutes
- 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
2026-02-08 00:02:21 -05:00
bc0467b1ff feat: increase HttpClient timeout to 5 minutes for large artist responses
- 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.
2026-02-08 00:01:08 -05:00
e057f365f4 feat: prefetch lyrics immediately after Odesli conversion
- 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
2026-02-07 23:52:49 -05:00
e8eb095a23 feat: move Odesli conversion to background after streaming starts
- 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
2026-02-07 23:51:03 -05:00
591fd5e8e1 feat: add 6 new SquidWTF endpoints and optimize Odesli conversion
- 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
2026-02-07 23:47:36 -05:00
3e840f987b fix: improve download speed and handle concurrent requests better
- 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.
2026-02-07 23:34:04 -05:00
56bc9d4ea9 fix: transparent proxy authentication and token expiration handling
- 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
2026-02-07 23:25:14 -05:00
f1dd01f6d5 refactor: reduce log spam by adjusting log levels
- 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.
2026-02-07 23:11:48 -05:00
6c06c59f61 Add enhanced logging for lyrics fetching to debug Jellyfin embedded lyrics check
Some checks failed
CI / build-and-test (push) Has been cancelled
- 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
2026-02-07 20:00:19 -05:00
56f2eca867 Fix endpoint usage parsing to show actual endpoints instead of HTTP methods
- Parse column 3 (endpoint path) instead of column 2 (HTTP method)
- Combine method and endpoint for clarity (e.g., 'GET users/{userId}')
- Now shows meaningful endpoint statistics instead of just GET/POST counts
2026-02-07 17:54:35 -05:00
248ab804f3 Add API Analytics tab to WebUI for endpoint usage tracking
- 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
2026-02-07 17:06:59 -05:00
b1769a35bf Fix download racing, cache cleanup, and WebUI storage mode display
- 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)
2026-02-07 16:45:28 -05:00
f741cc5297 fix: simplify ghost playback start to avoid Jellyfin validation errors
Some checks failed
CI / build-and-test (push) Has been cancelled
- 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
2026-02-07 13:30:47 -05:00
1a0e0216f5 feat: implement ghost item reporting for external track WebSocket sessions
- Generate deterministic UUIDs from external track IDs using MD5 hashing
- Create fake BaseItemDto objects with track metadata for external tracks
- Forward playback reports (start/progress/stop) to Jellyfin with ghost items
- Enables 'Now Playing' info in Jellyfin dashboard for external tracks
- Remove redundant JellyfinSessionManager WebSocket creation (client handles via proxy)
- Fix indentation issues in SquidWTF services (tabs to spaces)
- Add apis/*.md to .gitignore for temporary docs
- Fix null reference warning in provider switch expression
2026-02-07 13:27:25 -05:00
73bd3bf308 feat: add endpoint benchmarking on startup
- 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
2026-02-07 12:51:48 -05:00
43bf71c390 fix: race endpoints for download metadata fetching
- 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
2026-02-07 12:49:43 -05:00
2254616d32 feat: preserve source search ordering instead of re-scoring
- 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
2026-02-07 12:43:15 -05:00
c0444becad feat: add endpoint racing for downloads and searches
- 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
2026-02-07 12:36:50 -05:00
b906a5fd6d Refactor: Extract duplicate code into reusable helpers
- 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
2026-02-07 12:27:10 -05:00
e3bcc93597 Add Odesli service for Tidal to Spotify ID conversion
- 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
2026-02-07 12:19:41 -05:00
7e6bed51e1 refactor: extract Spotify ID from URL instead of entityUniqueId
- 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
2026-02-07 12:06:48 -05:00
47b9427c20 fix: extract Spotify track ID from Odesli entityUniqueId format
- 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'
2026-02-07 12:05:55 -05:00
bb46db43b1 fix: use persistent cache/Music folder instead of /tmp
- 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
2026-02-07 12:02:48 -05:00
3937e637c6 feat: convert Tidal tracks to Spotify ID immediately for lyrics
- 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
2026-02-07 11:57:24 -05:00
2272e8d363 fix: use stored session headers for WebSocket auth
- 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
2026-02-07 11:52:17 -05:00
6169d7a4ac fix: session capabilities using disposed HTTP context
- 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
2026-02-07 11:49:43 -05:00
da8cb29e08 refactor: make authentication truly transparent proxy
- 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
2026-02-07 11:45:16 -05:00
d88ed64e37 fix: pass through Jellyfin error responses to client
- 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
2026-02-07 11:42:11 -05:00
210d18220b fix: use case-insensitive provider key matching 2026-02-07 11:22:00 -05:00
c44e48a425 add debug logging to track provider identification 2026-02-07 11:21:03 -05:00
e44b46aee1 remove lyrics column from playlist table 2026-02-07 11:17:48 -05:00