diff --git a/SESSION_FIX_SUMMARY.md b/SESSION_FIX_SUMMARY.md deleted file mode 100644 index f513c91..0000000 --- a/SESSION_FIX_SUMMARY.md +++ /dev/null @@ -1,292 +0,0 @@ -# Session Management Fix for External Tracks - Summary - -## Problem Statement - -Previously, when users played external tracks (from Deezer, Qobuz, or SquidWTF), no session was created in Jellyfin. This caused: -- Client not appearing in Jellyfin dashboard -- No remote control capabilities -- No session tracking for external playback -- Inconsistent behavior between local and external tracks - -## Root Cause - -In `JellyfinController.ReportPlaybackStart()`, when an external track was detected, the code would: -1. Log the playback start -2. Prefetch lyrics -3. Return `NoContent()` immediately - -**Missing**: Session creation and WebSocket connection establishment - -## Solution Implemented - -### 1. Session Creation for External Tracks -**Location**: `allstarr/Controllers/JellyfinController.cs:2210-2236` - -Added session creation logic for external tracks: -```csharp -// CRITICAL: Create session for external tracks too! -if (!string.IsNullOrEmpty(deviceId)) -{ - _logger.LogInformation("๐Ÿ”ง SESSION: Creating session for external track playback"); - var sessionCreated = await _sessionManager.EnsureSessionAsync( - deviceId, - client ?? "Unknown", - device ?? "Unknown", - version ?? "1.0", - Request.Headers); - - if (sessionCreated) - { - _logger.LogInformation("โœ“ SESSION: Session created for external track playback on device {DeviceId}", deviceId); - } -} -``` - -### 2. Session Activity Updates -**Location**: `allstarr/Controllers/JellyfinController.cs:2391-2409` - -Added session activity updates during progress reports: -```csharp -// For external tracks, update session activity to keep it alive -if (!string.IsNullOrEmpty(deviceId)) -{ - _sessionManager.UpdateActivity(deviceId); - - // Log progress occasionally for debugging (every ~30 seconds) - if (positionTicks.HasValue) - { - var position = TimeSpan.FromTicks(positionTicks.Value); - if (position.Seconds % 30 == 0 && position.Milliseconds < 500) - { - _logger.LogDebug("โ–ถ๏ธ External track progress: {Position:mm\\:ss} ({Provider}/{ExternalId})", - position, provider, externalId); - } - } -} -``` - -### 3. Existing Session Cleanup (Already Working) -**Location**: `allstarr/Controllers/JellyfinController.cs:2458-2461` - -Session cleanup was already implemented: -```csharp -if (!string.IsNullOrEmpty(deviceId)) -{ - _sessionManager.MarkSessionPotentiallyEnded(deviceId, TimeSpan.FromSeconds(50)); -} -``` - -## How It Works - -### Session Creation Flow -1. **Client plays external track** โ†’ POST `/Sessions/Playing` -2. **Controller detects external track** โ†’ Parses `ext-provider-song-id` format -3. **Extract device info** โ†’ From `X-Emby-Authorization` header -4. **Create session** โ†’ `_sessionManager.EnsureSessionAsync()` -5. **Session manager**: - - Posts capabilities to Jellyfin (creates session) - - Stores session info in memory - - **Starts WebSocket connection** to Jellyfin -6. **WebSocket initialization**: - - Connects to Jellyfin WebSocket endpoint - - Sends `ForceKeepAlive` message - - Sends `SessionsStart` message - - Maintains connection with periodic keep-alives -7. **Session appears in Jellyfin dashboard** - -### WebSocket Connection Details -The `JellyfinSessionManager.MaintainWebSocketForSessionAsync()` method: -- Authenticates using **client's token** (not server API key) -- Sends initialization messages to Jellyfin -- Receives remote control commands -- Sends keep-alive every 30 seconds -- Handles disconnections gracefully - -### Session Lifecycle -- **Created**: On first external track playback -- **Active**: Updated every progress report (~1-5 seconds) -- **Stale**: After 3 minutes of inactivity -- **Cleanup**: Automatically removed after grace period - -## Testing - -### Build and Test Results -```bash -โœ… Build: Successful (2.6s) -โœ… Tests: 225 passed, 0 failed -โœ… No diagnostics or warnings -``` - -### Manual Testing Checklist -See `TESTING_SESSION_MANAGEMENT.md` for comprehensive testing instructions: -- โœ… External track session creation -- โœ… Session persistence during playback -- โœ… Session cleanup after playback -- โœ… Session continuity between tracks -- โœ… Mixed local and external tracks -- โœ… Multiple clients - -## Code Quality - -### Changes Made -- **Files Modified**: 1 (`allstarr/Controllers/JellyfinController.cs`) -- **Lines Added**: ~50 -- **Lines Removed**: ~3 -- **Net Change**: +47 lines - -### Code Review Checklist -- โœ… Follows existing code patterns -- โœ… Comprehensive logging added -- โœ… Error handling in place -- โœ… No breaking changes -- โœ… Backward compatible -- โœ… All tests pass -- โœ… No new dependencies - -### Logging Improvements -Added detailed logging for debugging: -- `๐Ÿ”ง SESSION: Creating session for external track playback` -- `โœ“ SESSION: Session created for external track playback on device {DeviceId}` -- `โš ๏ธ SESSION: Failed to create session for external track playback` -- `โš ๏ธ SESSION: No device ID found for external track playback` -- `โ–ถ๏ธ External track progress: {Position} ({Provider}/{ExternalId})` - -## Architecture Impact - -### No Breaking Changes -- Existing local track behavior unchanged -- External track streaming unchanged -- Session management for local tracks unchanged -- WebSocket proxy middleware unchanged - -### Performance Impact -- **Memory**: +10-50 KB per active session -- **Network**: +1 KB/minute per session (WebSocket keep-alive) -- **CPU**: Negligible (async operations) - -### Scalability -- Tested with 10 concurrent sessions -- No performance degradation -- Sessions auto-cleanup prevents memory leaks - -## Documentation - -### Files Created -1. **TESTING_SESSION_MANAGEMENT.md** (350+ lines) - - Comprehensive testing guide - - Architecture explanation - - Debugging instructions - - Common issues and solutions - -2. **SESSION_FIX_SUMMARY.md** (this file) - - Problem statement - - Solution overview - - Implementation details - -### Existing Documentation Updated -- None (no changes to existing docs needed) - -## Git Workflow - -### Commit Details -``` -Branch: dev -Commit: 73509eb -Message: feat: create sessions and WebSocket connections for external track playback - -- 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 -``` - -### Next Steps (Per GIT.md) -1. โœ… Committed to `dev` branch -2. โณ Test thoroughly with real clients -3. โณ Squash merge to `beta` for staging testing -4. โณ Squash merge to `main` for production release - -## Benefits - -### User Experience -- โœ… External track playback now visible in Jellyfin dashboard -- โœ… Consistent behavior between local and external tracks -- โœ… Remote control capabilities for external playback -- โœ… Session tracking for all playback types - -### Developer Experience -- โœ… Clear logging for debugging -- โœ… Comprehensive testing documentation -- โœ… Clean, maintainable code -- โœ… No breaking changes - -### System Reliability -- โœ… Automatic session cleanup -- โœ… Graceful error handling -- โœ… WebSocket reconnection logic -- โœ… Memory leak prevention - -## Known Limitations - -### Playback Info Not Displayed -**Issue**: Jellyfin dashboard shows session but not "Now Playing" info for external tracks - -**Reason**: Jellyfin doesn't know about external tracks (they're not in its database) - -**Impact**: Low - session is visible and controllable, just no track details shown - -**Future Fix**: Could spoof generic playback info to Jellyfin (see Future Improvements) - -### Remote Control Limited -**Issue**: Some remote control commands may not work for external tracks - -**Reason**: Commands like "Play Next" require Jellyfin to know the queue - -**Impact**: Low - basic controls (play/pause/stop) work via client - -**Future Fix**: Implement command handling in Allstarr proxy - -## Future Improvements - -1. **Spoofed Playback Info** - - Send generic "Now Playing" data to Jellyfin - - Display track name, artist, album in dashboard - - Requires creating temporary Jellyfin items - -2. **Remote Control Support** - - Handle remote control commands from Jellyfin - - Implement play/pause/stop/seek for external tracks - - Requires WebSocket message handling - -3. **Session Persistence** - - Store sessions in Redis for multi-instance deployments - - Survive Allstarr restarts - - Share sessions across multiple Allstarr instances - -4. **Metrics and Monitoring** - - Track session creation/cleanup rates - - Monitor WebSocket connection health - - Alert on session failures - -5. **Enhanced Logging** - - Structured logging for better analysis - - Session lifecycle events - - Performance metrics - -## Conclusion - -This fix successfully implements session management for external track playback, bringing feature parity with local track playback. The implementation is clean, well-tested, and follows existing code patterns. Users will now see their clients in the Jellyfin dashboard when playing external tracks, providing a more consistent and professional experience. - -### Success Criteria Met -- โœ… Sessions created for external tracks -- โœ… WebSocket connections established -- โœ… Sessions visible in Jellyfin dashboard -- โœ… Session cleanup working correctly -- โœ… No breaking changes -- โœ… All tests passing -- โœ… Comprehensive documentation - -### Ready for Testing -The code is ready for manual testing with real clients. Follow the instructions in `TESTING_SESSION_MANAGEMENT.md` to verify the functionality works as expected. diff --git a/TESTING_SESSION_MANAGEMENT.md b/TESTING_SESSION_MANAGEMENT.md deleted file mode 100644 index f29444a..0000000 --- a/TESTING_SESSION_MANAGEMENT.md +++ /dev/null @@ -1,326 +0,0 @@ -# Testing Session Management for External Tracks - -## Overview - -This document describes how to test the session management improvements for external tracks. Previously, sessions were only created for local tracks, causing external track playback to not appear in the Jellyfin dashboard. - -## Changes Made - -### 1. Session Creation for External Tracks -**File**: `allstarr/Controllers/JellyfinController.cs` -**Method**: `ReportPlaybackStart()` - -When an external track starts playing, the system now: -1. Extracts device information from request headers -2. Calls `_sessionManager.EnsureSessionAsync()` to create/ensure a session exists -3. The session manager creates a WebSocket connection to Jellyfin -4. The session appears in the Jellyfin dashboard - -### 2. Session Activity Updates -**File**: `allstarr/Controllers/JellyfinController.cs` -**Method**: `ReportPlaybackProgress()` - -During external track playback: -1. Session activity is updated every progress report -2. This keeps the session alive and visible in the dashboard -3. Progress is logged every ~30 seconds for debugging - -### 3. Session Cleanup -**File**: `allstarr/Controllers/JellyfinController.cs` -**Method**: `ReportPlaybackStopped()` - -When external track playback stops: -1. Session is marked as potentially ended -2. 50-second grace period allows next track to start -3. If no activity, session is automatically cleaned up - -## How Sessions Work - -### Session Creation Flow -``` -1. Client plays external track - โ†“ -2. POST /Sessions/Playing with external itemId - โ†“ -3. Controller detects external track (ext-provider-song-id) - โ†“ -4. Extract device info from X-Emby-Authorization header - โ†“ -5. Call _sessionManager.EnsureSessionAsync() - โ†“ -6. Session manager: - - Posts capabilities to Jellyfin (creates session) - - Stores session info in memory - - Starts WebSocket connection to Jellyfin - โ†“ -7. Session appears in Jellyfin dashboard -``` - -### WebSocket Connection -The session manager maintains a WebSocket connection to Jellyfin: -- Authenticates using client's token (not server API key) -- Sends `ForceKeepAlive` message to initialize session -- Sends `SessionsStart` to subscribe to updates -- Sends periodic `KeepAlive` messages every 30 seconds -- Receives remote control commands from Jellyfin - -### Session Lifecycle -- **Created**: On first playback start -- **Active**: Updated on every progress report -- **Stale**: After 3 minutes of inactivity -- **Cleanup**: Automatically removed after grace period - -## Testing Instructions - -### Prerequisites -1. Allstarr running with external provider configured (Deezer/Qobuz/SquidWTF) -2. Jellyfin server accessible -3. Music client (Feishin, Finamp, etc.) - -### Test Case 1: External Track Session Creation - -**Steps:** -1. Open Jellyfin dashboard in browser -2. Navigate to Dashboard โ†’ Active Devices -3. In your music client, search for a track not in your local library -4. Play the external track -5. Check Jellyfin dashboard - -**Expected Results:** -- Session appears in Active Devices list -- Shows client name, device name -- Shows "Now Playing" status -- Session remains visible during playback - -**Logs to Check:** -``` -๐Ÿ”ง SESSION: Creating session for external track playback -โœ“ SESSION: Session created for external track playback on device {DeviceId} -๐Ÿ”Œ WEBSOCKET: Connected to Jellyfin for device {DeviceId} -๐Ÿ“ค WEBSOCKET: Sent ForceKeepAlive to initialize session for {DeviceId} -``` - -### Test Case 2: Session Persistence During Playback - -**Steps:** -1. Play an external track -2. Let it play for 1-2 minutes -3. Monitor Jellyfin dashboard -4. Check logs for progress updates - -**Expected Results:** -- Session remains visible throughout playback -- No disconnections or reconnections -- Progress updates logged every ~30 seconds - -**Logs to Check:** -``` -โ–ถ๏ธ External track progress: 00:30 (provider/externalId) -โ–ถ๏ธ External track progress: 01:00 (provider/externalId) -๐Ÿ’“ WEBSOCKET: Sent KeepAlive for {DeviceId} -``` - -### Test Case 3: Session Cleanup After Playback - -**Steps:** -1. Play an external track -2. Stop playback (don't play another track) -3. Wait 50 seconds -4. Check Jellyfin dashboard - -**Expected Results:** -- Session marked as potentially ended -- After 50 seconds, session removed from dashboard -- WebSocket connection closed cleanly - -**Logs to Check:** -``` -โน๏ธ Playback STOPPED reported -๐ŸŽต External track playback stopped: {Name} at {Position} -โฐ SESSION: Marking session {DeviceId} as potentially ended, will cleanup in 50s if no activity -๐Ÿงน SESSION: Auto-removing inactive session {DeviceId} after playback stop -๐Ÿ”Œ WEBSOCKET: Closed WebSocket for device {DeviceId} -``` - -### Test Case 4: Session Continuity Between Tracks - -**Steps:** -1. Create a playlist with external tracks -2. Play the playlist -3. Let it play through 2-3 tracks -4. Monitor Jellyfin dashboard - -**Expected Results:** -- Single session persists across tracks -- No session recreation between tracks -- Smooth transitions without disconnections - -**Logs to Check:** -``` -โœ“ SESSION: Session already exists for device {DeviceId} -๐Ÿ”„ SESSION: Updated activity for {DeviceId} -``` - -### Test Case 5: Mixed Local and External Tracks - -**Steps:** -1. Create a playlist with both local and external tracks -2. Play the playlist -3. Monitor session behavior during transitions - -**Expected Results:** -- Session persists across both local and external tracks -- Playback reports sent to Jellyfin for local tracks -- Session activity updated for external tracks -- No session interruptions - -### Test Case 6: Multiple Clients - -**Steps:** -1. Connect two different clients (e.g., Feishin + Finamp) -2. Play external tracks on both -3. Check Jellyfin dashboard - -**Expected Results:** -- Two separate sessions appear -- Each session tracked independently -- No interference between sessions - -## Debugging - -### Enable Debug Logging -In `appsettings.json`: -```json -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "allstarr.Services.Jellyfin.JellyfinSessionManager": "Debug", - "allstarr.Middleware.WebSocketProxyMiddleware": "Debug" - } - } -} -``` - -### Check Session Status -GET `/admin/sessions` endpoint returns: -```json -{ - "TotalSessions": 1, - "ActiveSessions": 1, - "StaleSessions": 0, - "Sessions": [ - { - "DeviceId": "device-123", - "Client": "Feishin", - "Device": "Chrome", - "Version": "0.10.0", - "LastActivity": "2026-02-06T12:34:56Z", - "InactiveMinutes": 0.5, - "HasWebSocket": true, - "WebSocketState": "Open" - } - ] -} -``` - -### Common Issues - -#### Session Not Appearing -**Symptom**: External track plays but no session in dashboard -**Causes**: -- No device ID in request headers -- Authentication headers missing -- Jellyfin connection failed - -**Check**: -``` -โš ๏ธ SESSION: No device ID found for external track playback -โš ๏ธ SESSION: Failed to create session for external track playback -โŒ WEBSOCKET: Failed to maintain WebSocket for device {DeviceId} -``` - -#### Session Disconnects Frequently -**Symptom**: Session appears and disappears -**Causes**: -- WebSocket connection unstable -- Network issues -- Jellyfin server restarting - -**Check**: -``` -โš ๏ธ WEBSOCKET: WebSocket error for device {DeviceId} -โš ๏ธ WEBSOCKET: Connection closed prematurely -``` - -#### Session Not Cleaning Up -**Symptom**: Old sessions remain in dashboard -**Causes**: -- Grace period too long -- Session activity still being updated -- WebSocket not closing properly - -**Check**: -``` -๐Ÿงน SESSION: Removing stale session for {DeviceId} (inactive for X minutes) -``` - -## Architecture Notes - -### Why WebSocket is Required -Jellyfin requires an active WebSocket connection for a session to: -1. Appear in the dashboard -2. Receive remote control commands -3. Show real-time playback status -4. Enable features like "Play on Device" - -Without WebSocket, the session exists in Jellyfin's database but is not visible or controllable. - -### Session vs Playback Reporting -- **Session**: Represents a connected client (created via capabilities + WebSocket) -- **Playback Reporting**: Tells Jellyfin what's playing (POST /Sessions/Playing) - -For external tracks: -- โœ… Session is created (client appears in dashboard) -- โŒ Playback is NOT reported (Jellyfin doesn't know the track) - -This is intentional - Jellyfin can't display "Now Playing" info for tracks it doesn't know about, but the client session is still visible and controllable. - -### Authentication Flow -1. Client sends `X-Emby-Authorization` header with token -2. Allstarr forwards this to Jellyfin for session creation -3. WebSocket uses same token to authenticate -4. Session appears under the correct user in Jellyfin - -**Important**: We use the client's token, NOT the server API key, so the session appears under the actual user, not the admin. - -## Performance Considerations - -### Memory Usage -- Each session stores: device info, headers, WebSocket connection -- Typical memory per session: ~10-50 KB -- Sessions auto-cleanup after 3 minutes of inactivity - -### Network Usage -- WebSocket: ~1 KB/minute (keep-alive messages) -- Session capabilities: ~500 bytes every 10 seconds -- Minimal overhead for typical usage - -### Scalability -- Tested with up to 10 concurrent sessions -- No performance degradation observed -- WebSocket connections are lightweight - -## Future Improvements - -1. **Spoofed Playback Info**: Send generic "Now Playing" data to Jellyfin for external tracks -2. **Session Persistence**: Store sessions in Redis for multi-instance deployments -3. **Remote Control**: Handle remote control commands for external tracks -4. **Metrics**: Track session creation/cleanup rates -5. **Health Checks**: Monitor WebSocket connection health - -## References - -- Jellyfin Session API: https://api.jellyfin.org/#tag/Session -- WebSocket Protocol: https://jellyfin.org/docs/general/networking/websocket -- Allstarr Architecture: `apis/steering/ARCHITECTURE.md` diff --git a/allstarr/Controllers/JellyfinController.cs b/allstarr/Controllers/JellyfinController.cs index 904c901..931a890 100644 --- a/allstarr/Controllers/JellyfinController.cs +++ b/allstarr/Controllers/JellyfinController.cs @@ -90,7 +90,7 @@ public class JellyfinController : ControllerBase /// /// Searches local Jellyfin library and external providers. - /// Dedupes artists, combines songs/albums. Works with /Items and /Users/{userId}/Items. + /// Combines songs/albums/artists. Works with /Items and /Users/{userId}/Items. /// [HttpGet("Items", Order = 1)] [HttpGet("Users/{userId}/Items", Order = 1)] @@ -294,53 +294,19 @@ public class JellyfinController : ControllerBase .Select(x => x.Item) .ToList(); - // Dedupe artists by name, but KEEP both local and external versions - // Group by name, then for each name keep ONE local and ONE external (if both exist) - var artistsByName = scoredLocalArtists.Concat(scoredExternalArtists) - .GroupBy(x => x.Item.Name, StringComparer.OrdinalIgnoreCase); - - var artistScores = new List(); - foreach (var group in artistsByName) - { - // Get best local artist (if any) - var bestLocal = group - .Where(x => x.Item.IsLocal) - .OrderByDescending(x => x.Score) - .FirstOrDefault(); - - // Get best external artist (if any) - var bestExternal = group - .Where(x => !x.Item.IsLocal) - .OrderByDescending(x => x.Score) - .FirstOrDefault(); - - // Add both (if they exist) - if (bestLocal.Item != null) - { - artistScores.Add(bestLocal.Item); - } - if (bestExternal.Item != null) - { - artistScores.Add(bestExternal.Item); - } - } - - // Sort by score - artistScores = artistScores - .OrderByDescending(a => { - // Find the score for this artist - var scored = scoredLocalArtists.Concat(scoredExternalArtists) - .FirstOrDefault(x => x.Item.Id == a.Id); - return scored.Score; - }) + // NO deduplication - just merge and sort by relevance score + // Show ALL matches (local + external) sorted by best match first + var artistScores = scoredLocalArtists.Concat(scoredExternalArtists) + .OrderByDescending(x => x.Score) + .Select(x => x.Item) .ToList(); - // Log deduplication details for debugging + // Log results for debugging if (_logger.IsEnabled(LogLevel.Debug)) { var localArtistNames = scoredLocalArtists.Select(x => $"{x.Item.Name} (local, score: {x.Score:F2})").ToList(); var externalArtistNames = scoredExternalArtists.Select(x => $"{x.Item.Name} ({x.Item.ExternalProvider}, score: {x.Score:F2})").ToList(); - _logger.LogDebug("๐ŸŽค Artist deduplication: Local={LocalArtists}, External={ExternalArtists}, Final={FinalCount}", + _logger.LogDebug("๐ŸŽค Artist results: Local={LocalArtists}, External={ExternalArtists}, Total={TotalCount}", string.Join(", ", localArtistNames), string.Join(", ", externalArtistNames), artistScores.Count); @@ -536,20 +502,10 @@ public class JellyfinController : ControllerBase var (localSongs, localAlbums, localArtists) = _modelMapper.ParseItemsResponse(jellyfinResult); - // Merge and convert to search hints format + // NO deduplication - merge all results and take top matches var allSongs = localSongs.Concat(externalResult.Songs).Take(limit).ToList(); var allAlbums = localAlbums.Concat(externalResult.Albums).Take(limit).ToList(); - - // Dedupe artists by name - var artistNames = new HashSet(StringComparer.OrdinalIgnoreCase); - var allArtists = new List(); - foreach (var artist in localArtists.Concat(externalResult.Artists)) - { - if (artistNames.Add(artist.Name)) - { - allArtists.Add(artist); - } - } + var allArtists = localArtists.Concat(externalResult.Artists).Take(limit).ToList(); return _responseBuilder.CreateSearchHintsResponse( allSongs.Take(limit).ToList(), @@ -724,30 +680,11 @@ public class JellyfinController : ControllerBase } } - // Merge and deduplicate by name, but KEEP both local and external versions - // This allows users to see both their local "Taylor Swift" and external "Taylor Swift [S]" - var artistNames = new HashSet(StringComparer.OrdinalIgnoreCase); - var mergedArtists = new List(); + // NO deduplication - merge all artists and sort by relevance + // Show ALL matches (local + external) sorted by best match first + var mergedArtists = localArtists.Concat(externalArtists).ToList(); - // Add all local artists first - foreach (var artist in localArtists) - { - if (artistNames.Add(artist.Name + ":local")) - { - mergedArtists.Add(artist); - } - } - - // Add all external artists (even if name matches local) - foreach (var artist in externalArtists) - { - if (artistNames.Add(artist.Name + ":external")) - { - mergedArtists.Add(artist); - } - } - - _logger.LogInformation("Returning {Count} merged artists (local + external)", mergedArtists.Count); + _logger.LogInformation("Returning {Count} total artists (local + external, no deduplication)", mergedArtists.Count); // Convert to Jellyfin format var artistItems = mergedArtists.Select(a => _responseBuilder.ConvertArtistToJellyfinItem(a)).ToList(); @@ -1114,7 +1051,8 @@ public class JellyfinController : ControllerBase if (imageBytes == null || contentType == null) { - return NotFound(); + // Return placeholder if Jellyfin doesn't have image + return await GetPlaceholderImageAsync(); } return File(imageBytes, contentType); @@ -1131,7 +1069,8 @@ public class JellyfinController : ControllerBase if (string.IsNullOrEmpty(coverUrl)) { - return NotFound(); + // Return placeholder "no image available" image + return await GetPlaceholderImageAsync(); } // Fetch and return the image using the proxy service's HttpClient @@ -1140,7 +1079,8 @@ public class JellyfinController : ControllerBase var response = await _proxyService.HttpClient.GetAsync(coverUrl); if (!response.IsSuccessStatusCode) { - return NotFound(); + // Return placeholder on fetch failure + return await GetPlaceholderImageAsync(); } var imageBytes = await response.Content.ReadAsByteArrayAsync(); @@ -1150,10 +1090,34 @@ public class JellyfinController : ControllerBase catch (Exception ex) { _logger.LogWarning(ex, "Failed to fetch cover art from {Url}", coverUrl); - return NotFound(); + // Return placeholder on exception + return await GetPlaceholderImageAsync(); } } + /// + /// Returns a placeholder "no image available" image. + /// Generates a simple 1x1 transparent PNG as a minimal placeholder. + /// TODO: Replace with actual "no image available" graphic from wwwroot/placeholder.png + /// + private async Task GetPlaceholderImageAsync() + { + // Check if custom placeholder exists in wwwroot + var placeholderPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "placeholder.png"); + if (System.IO.File.Exists(placeholderPath)) + { + var imageBytes = await System.IO.File.ReadAllBytesAsync(placeholderPath); + return File(imageBytes, "image/png"); + } + + // Fallback: Return a 1x1 transparent PNG as minimal placeholder + var transparentPng = Convert.FromBase64String( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + ); + + return File(transparentPng, "image/png"); + } + #endregion #region Lyrics diff --git a/allstarr/wwwroot/placeholder.png b/allstarr/wwwroot/placeholder.png new file mode 100644 index 0000000..5bf929a Binary files /dev/null and b/allstarr/wwwroot/placeholder.png differ