diff --git a/allstarr/Controllers/JellyfinController.cs b/allstarr/Controllers/JellyfinController.cs index 13e2fc8..3e9fe10 100644 --- a/allstarr/Controllers/JellyfinController.cs +++ b/allstarr/Controllers/JellyfinController.cs @@ -113,12 +113,18 @@ public class JellyfinController : ControllerBase endpoint = $"{endpoint}{Request.QueryString.Value}"; } - var browseResult = await _proxyService.GetJsonAsync(endpoint, null, Request.Headers); + var (browseResult, statusCode) = await _proxyService.GetJsonAsync(endpoint, null, Request.Headers); if (browseResult == null) { - _logger.LogInformation("Jellyfin returned null - likely 401 Unauthorized, returning 401 to client"); - return Unauthorized(new { error = "Authentication required" }); + if (statusCode == 401) + { + _logger.LogInformation("Jellyfin returned 401 Unauthorized, returning 401 to client"); + return Unauthorized(new { error = "Authentication required" }); + } + + _logger.LogInformation("Jellyfin returned {StatusCode}, returning empty result", statusCode); + return new JsonResult(new { Items = Array.Empty(), TotalRecordCount = 0, StartIndex = startIndex }); } // Update Spotify playlist counts if enabled and response contains playlists @@ -168,7 +174,7 @@ public class JellyfinController : ControllerBase await Task.WhenAll(jellyfinTask, externalTask, playlistTask); - var jellyfinResult = await jellyfinTask; + var (jellyfinResult, _) = await jellyfinTask; var externalResult = await externalTask; var playlistResult = await playlistTask; @@ -315,7 +321,7 @@ public class JellyfinController : ControllerBase } // Proxy to Jellyfin for local content - var result = await _proxyService.GetItemsAsync( + var (result, statusCode) = await _proxyService.GetItemsAsync( parentId: parentId, includeItemTypes: ParseItemTypes(includeItemTypes), sortBy: sortBy, @@ -323,12 +329,7 @@ public class JellyfinController : ControllerBase startIndex: startIndex, clientHeaders: Request.Headers); - if (result == null) - { - return _responseBuilder.CreateError(404, "Parent not found"); - } - - return new JsonResult(JsonSerializer.Deserialize(result.RootElement.GetRawText())); + return HandleProxyResponse(result, statusCode); } /// @@ -360,7 +361,7 @@ public class JellyfinController : ControllerBase await Task.WhenAll(jellyfinTask, externalTask); - var jellyfinResult = await jellyfinTask; + var (jellyfinResult, _) = await jellyfinTask; var externalResult = await externalTask; var (localSongs, localAlbums, localArtists) = _modelMapper.ParseItemsResponse(jellyfinResult); @@ -416,13 +417,9 @@ public class JellyfinController : ControllerBase } // Proxy to Jellyfin - var result = await _proxyService.GetItemAsync(itemId, Request.Headers); - if (result == null) - { - return _responseBuilder.CreateError(404, "Item not found"); - } - - return new JsonResult(JsonSerializer.Deserialize(result.RootElement.GetRawText())); + var (result, statusCode) = await _proxyService.GetItemAsync(itemId, Request.Headers); + + return HandleProxyResponse(result, statusCode); } /// @@ -534,7 +531,7 @@ public class JellyfinController : ControllerBase await Task.WhenAll(jellyfinTask, externalTask); - var jellyfinResult = await jellyfinTask; + var (jellyfinResult, _) = await jellyfinTask; var externalArtists = await externalTask; _logger.LogInformation("Artist search results: Jellyfin={JellyfinCount}, External={ExternalCount}", @@ -584,19 +581,14 @@ public class JellyfinController : ControllerBase } // No search term - just proxy to Jellyfin - var result = await _proxyService.GetArtistsAsync(searchTerm, limit, startIndex, Request.Headers); + var (result, statusCode) = await _proxyService.GetArtistsAsync(searchTerm, limit, startIndex, Request.Headers); - if (result == null) + return HandleProxyResponse(result, statusCode, new { - return new JsonResult(new Dictionary - { - ["Items"] = Array.Empty(), - ["TotalRecordCount"] = 0, - ["StartIndex"] = startIndex - }); - } - - return new JsonResult(JsonSerializer.Deserialize(result.RootElement.GetRawText())); + Items = Array.Empty(), + TotalRecordCount = 0, + StartIndex = startIndex + }); } /// @@ -627,10 +619,10 @@ public class JellyfinController : ControllerBase } // Get local artist from Jellyfin - var jellyfinArtist = await _proxyService.GetArtistAsync(artistIdOrName, Request.Headers); + var (jellyfinArtist, statusCode) = await _proxyService.GetArtistAsync(artistIdOrName, Request.Headers); if (jellyfinArtist == null) { - return _responseBuilder.CreateError(404, "Artist not found"); + return HandleProxyResponse(null, statusCode); } var artistData = _modelMapper.ParseArtist(jellyfinArtist.RootElement); @@ -638,7 +630,7 @@ public class JellyfinController : ControllerBase var localArtistId = artistData.Id; // Get local albums - var localAlbumsResult = await _proxyService.GetItemsAsync( + var (localAlbumsResult, _) = await _proxyService.GetItemsAsync( parentId: null, includeItemTypes: new[] { "MusicAlbum" }, sortBy: "SortName", @@ -992,7 +984,7 @@ public class JellyfinController : ControllerBase else { // For local songs, get metadata from Jellyfin - var item = await _proxyService.GetItemAsync(itemId, Request.Headers); + var (item, _) = await _proxyService.GetItemAsync(itemId, Request.Headers); if (item != null && item.RootElement.TryGetProperty("Type", out var typeEl) && typeEl.GetString() == "Audio") { @@ -1213,15 +1205,9 @@ public class JellyfinController : ControllerBase _logger.LogInformation("Proxying favorite request to Jellyfin: {Endpoint}", endpoint); - var result = await _proxyService.PostJsonAsync(endpoint, "{}", Request.Headers); + var (result, statusCode) = await _proxyService.PostJsonAsync(endpoint, "{}", Request.Headers); - if (result == null) - { - _logger.LogWarning("Failed to favorite item in Jellyfin - proxy returned null"); - return StatusCode(500, new { error = "Failed to mark favorite" }); - } - - return new JsonResult(JsonSerializer.Deserialize(result.RootElement.GetRawText())); + return HandleProxyResponse(result, statusCode); } /// @@ -1263,20 +1249,13 @@ public class JellyfinController : ControllerBase _logger.LogInformation("Proxying unfavorite request to Jellyfin: {Endpoint}", endpoint); - var result = await _proxyService.DeleteAsync(endpoint, Request.Headers); + var (result, statusCode) = await _proxyService.DeleteAsync(endpoint, Request.Headers); - if (result == null) - { - // DELETE often returns 204 No Content, which is success - _logger.LogInformation("Unfavorite succeeded (no content returned)"); - return Ok(new - { - IsFavorite = false, - ItemId = itemId - }); - } - - return new JsonResult(JsonSerializer.Deserialize(result.RootElement.GetRawText())); + return HandleProxyResponse(result, statusCode, new + { + IsFavorite = false, + ItemId = itemId + }); } #endregion @@ -1349,7 +1328,7 @@ public class JellyfinController : ControllerBase { // Get playlist info from Jellyfin to get the name for matching missing tracks _logger.LogInformation("Fetching playlist info from Jellyfin for ID: {PlaylistId}", playlistId); - var playlistInfo = await _proxyService.GetJsonAsync($"Items/{playlistId}", null, Request.Headers); + var (playlistInfo, _) = await _proxyService.GetJsonAsync($"Items/{playlistId}", null, Request.Headers); if (playlistInfo != null && playlistInfo.RootElement.TryGetProperty("Name", out var nameElement)) { @@ -1372,13 +1351,9 @@ public class JellyfinController : ControllerBase } _logger.LogInformation("Proxying to Jellyfin: {Endpoint}", endpoint); - var result = await _proxyService.GetJsonAsync(endpoint, null, Request.Headers); - if (result == null) - { - return _responseBuilder.CreateError(404, "Playlist not found"); - } + var (result, statusCode) = await _proxyService.GetJsonAsync(endpoint, null, Request.Headers); - return new JsonResult(JsonSerializer.Deserialize(result.RootElement.GetRawText())); + return HandleProxyResponse(result, statusCode); } catch (Exception ex) { @@ -1446,12 +1421,16 @@ public class JellyfinController : ControllerBase // DO NOT log request body or detailed headers - contains password // Forward to Jellyfin server with client headers - var result = await _proxyService.PostJsonAsync("Users/AuthenticateByName", body, Request.Headers); + var (result, statusCode) = await _proxyService.PostJsonAsync("Users/AuthenticateByName", body, Request.Headers); if (result == null) { - _logger.LogWarning("Authentication failed - no response from Jellyfin"); - return Unauthorized(new { error = "Authentication failed" }); + _logger.LogWarning("Authentication failed - status {StatusCode}", statusCode); + if (statusCode == 401) + { + return Unauthorized(new { error = "Invalid username or password" }); + } + return StatusCode(statusCode, new { error = "Authentication failed" }); } _logger.LogInformation("Authentication successful"); @@ -1558,18 +1537,9 @@ public class JellyfinController : ControllerBase queryParams["userId"] = userId; } - var result = await _proxyService.GetJsonAsync(endpoint, queryParams, Request.Headers); + var (result, statusCode) = await _proxyService.GetJsonAsync(endpoint, queryParams, Request.Headers); - if (result == null) - { - return _responseBuilder.CreateJsonResponse(new - { - Items = Array.Empty(), - TotalRecordCount = 0 - }); - } - - return new JsonResult(JsonSerializer.Deserialize(result.RootElement.GetRawText())); + return HandleProxyResponse(result, statusCode, new { Items = Array.Empty(), TotalRecordCount = 0 }); } /// @@ -1672,18 +1642,228 @@ public class JellyfinController : ControllerBase queryParams["userId"] = userId; } - var result = await _proxyService.GetJsonAsync($"Songs/{itemId}/InstantMix", queryParams, Request.Headers); + var (result, statusCode) = await _proxyService.GetJsonAsync($"Songs/{itemId}/InstantMix", queryParams, Request.Headers); - if (result == null) - { - return _responseBuilder.CreateJsonResponse(new - { - Items = Array.Empty(), - TotalRecordCount = 0 - }); - } + return HandleProxyResponse(result, statusCode, new { Items = Array.Empty(), TotalRecordCount = 0 }); + } - return new JsonResult(JsonSerializer.Deserialize(result.RootElement.GetRawText())); + #endregion + + #region Playback Session Reporting + + /// + /// Reports playback start. Handles both local and external tracks. + /// For local tracks, forwards to Jellyfin. For external tracks, logs locally. + /// + [HttpPost("Sessions/Playing")] + public async Task ReportPlaybackStart() + { + try + { + Request.EnableBuffering(); + string body; + using (var reader = new StreamReader(Request.Body, System.Text.Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 1024, leaveOpen: true)) + { + body = await reader.ReadToEndAsync(); + } + Request.Body.Position = 0; + + _logger.LogInformation("đŸ“ģ Playback START reported"); + + // Parse the body to check if it's an external track + var doc = JsonDocument.Parse(body); + string? itemId = null; + string? itemName = null; + + if (doc.RootElement.TryGetProperty("ItemId", out var itemIdProp)) + { + itemId = itemIdProp.GetString(); + } + + if (doc.RootElement.TryGetProperty("ItemName", out var itemNameProp)) + { + itemName = itemNameProp.GetString(); + } + + if (!string.IsNullOrEmpty(itemId)) + { + var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(itemId); + + if (isExternal) + { + _logger.LogInformation("đŸŽĩ External track playback started: {Name} ({Provider}/{ExternalId})", + itemName ?? "Unknown", provider, externalId); + // For external tracks, we can't report to Jellyfin since it doesn't know about them + // Just return success so the client is happy + return NoContent(); + } + + _logger.LogInformation("đŸŽĩ Local track playback started: {Name} (ID: {ItemId})", + itemName ?? "Unknown", itemId); + } + + // For local tracks, forward to Jellyfin with client auth + _logger.LogInformation("Forwarding playback start to Jellyfin..."); + var (result, statusCode) = await _proxyService.PostJsonAsync("Sessions/Playing", body, Request.Headers); + + if (statusCode == 204 || statusCode == 200) + { + _logger.LogInformation("✓ Playback start forwarded to Jellyfin ({StatusCode})", statusCode); + } + else + { + _logger.LogWarning("Playback start forward failed with status {StatusCode}", statusCode); + } + + return NoContent(); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to report playback start"); + return NoContent(); // Return success anyway to not break playback + } + } + + /// + /// Reports playback progress. Handles both local and external tracks. + /// + [HttpPost("Sessions/Playing/Progress")] + public async Task ReportPlaybackProgress() + { + try + { + Request.EnableBuffering(); + string body; + using (var reader = new StreamReader(Request.Body, System.Text.Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 1024, leaveOpen: true)) + { + body = await reader.ReadToEndAsync(); + } + Request.Body.Position = 0; + + // Parse the body to check if it's an external track + var doc = JsonDocument.Parse(body); + if (doc.RootElement.TryGetProperty("ItemId", out var itemIdProp)) + { + var itemId = itemIdProp.GetString(); + var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(itemId ?? ""); + + if (isExternal) + { + // For external tracks, just acknowledge + return NoContent(); + } + } + + // For local tracks, forward to Jellyfin + var (result, statusCode) = await _proxyService.PostJsonAsync("Sessions/Playing/Progress", body, Request.Headers); + return NoContent(); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to report playback progress"); + return NoContent(); + } + } + + /// + /// Reports playback stopped. Handles both local and external tracks. + /// + [HttpPost("Sessions/Playing/Stopped")] + public async Task ReportPlaybackStopped() + { + try + { + Request.EnableBuffering(); + string body; + using (var reader = new StreamReader(Request.Body, System.Text.Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 1024, leaveOpen: true)) + { + body = await reader.ReadToEndAsync(); + } + Request.Body.Position = 0; + + _logger.LogInformation("âšī¸ Playback STOPPED reported"); + + // Parse the body to check if it's an external track + var doc = JsonDocument.Parse(body); + string? itemId = null; + string? itemName = null; + long? positionTicks = null; + + if (doc.RootElement.TryGetProperty("ItemId", out var itemIdProp)) + { + itemId = itemIdProp.GetString(); + } + + if (doc.RootElement.TryGetProperty("ItemName", out var itemNameProp)) + { + itemName = itemNameProp.GetString(); + } + + if (doc.RootElement.TryGetProperty("PositionTicks", out var posProp)) + { + positionTicks = posProp.GetInt64(); + } + + if (!string.IsNullOrEmpty(itemId)) + { + var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(itemId); + + if (isExternal) + { + var position = positionTicks.HasValue + ? TimeSpan.FromTicks(positionTicks.Value).ToString(@"mm\:ss") + : "unknown"; + _logger.LogInformation("đŸŽĩ External track playback stopped: {Name} at {Position} ({Provider}/{ExternalId})", + itemName ?? "Unknown", position, provider, externalId); + return NoContent(); + } + + _logger.LogInformation("đŸŽĩ Local track playback stopped: {Name} (ID: {ItemId})", + itemName ?? "Unknown", itemId); + } + + // For local tracks, forward to Jellyfin + _logger.LogInformation("Forwarding playback stop to Jellyfin..."); + var (result, statusCode) = await _proxyService.PostJsonAsync("Sessions/Playing/Stopped", body, Request.Headers); + + if (statusCode == 204 || statusCode == 200) + { + _logger.LogInformation("✓ Playback stop forwarded to Jellyfin ({StatusCode})", statusCode); + } + else + { + _logger.LogWarning("Playback stop forward failed with status {StatusCode}", statusCode); + } + + return NoContent(); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to report playback stopped"); + return NoContent(); + } + } + + /// + /// Pings a playback session to keep it alive. + /// + [HttpPost("Sessions/Playing/Ping")] + public async Task PingPlaybackSession([FromQuery] string playSessionId) + { + try + { + _logger.LogDebug("Playback session ping: {SessionId}", playSessionId); + + // Forward to Jellyfin + var endpoint = $"Sessions/Playing/Ping?playSessionId={Uri.EscapeDataString(playSessionId)}"; + var (result, statusCode) = await _proxyService.PostJsonAsync(endpoint, "{}", Request.Headers); + return NoContent(); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to ping playback session"); + return NoContent(); + } } #endregion @@ -1865,6 +2045,7 @@ public class JellyfinController : ControllerBase } JsonDocument? result; + int statusCode; if (HttpContext.Request.Method == HttpMethod.Post.Method) { @@ -1906,18 +2087,44 @@ public class JellyfinController : ControllerBase } } - result = await _proxyService.PostJsonAsync(fullPath, body, Request.Headers); + (result, statusCode) = await _proxyService.PostJsonAsync(fullPath, body, Request.Headers); } else { // Forward GET requests transparently with authentication headers and query string - result = await _proxyService.GetJsonAsync(fullPath, null, Request.Headers); + (result, statusCode) = await _proxyService.GetJsonAsync(fullPath, null, Request.Headers); } + // Handle different status codes if (result == null) { - // Return 204 No Content for successful requests with no body - // (e.g., /sessions/playing, /sessions/playing/progress) + // No body - return the status code from Jellyfin + if (statusCode == 204) + { + return NoContent(); + } + else if (statusCode == 401) + { + return Unauthorized(); + } + else if (statusCode == 403) + { + return Forbid(); + } + else if (statusCode == 404) + { + return NotFound(); + } + else if (statusCode >= 400 && statusCode < 500) + { + return StatusCode(statusCode); + } + else if (statusCode >= 500) + { + return StatusCode(statusCode); + } + + // Default to 204 for 2xx responses with no body return NoContent(); } @@ -1941,6 +2148,43 @@ public class JellyfinController : ControllerBase #region Helpers + /// + /// Helper to handle proxy responses with proper status code handling. + /// + private IActionResult HandleProxyResponse(JsonDocument? result, int statusCode, object? fallbackValue = null) + { + if (result != null) + { + return new JsonResult(JsonSerializer.Deserialize(result.RootElement.GetRawText())); + } + + // Handle error status codes + if (statusCode == 401) + { + return Unauthorized(); + } + else if (statusCode == 403) + { + return Forbid(); + } + else if (statusCode == 404) + { + return NotFound(); + } + else if (statusCode >= 400) + { + return StatusCode(statusCode); + } + + // Success with no body - return fallback or empty + if (fallbackValue != null) + { + return new JsonResult(fallbackValue); + } + + return NoContent(); + } + /// /// Updates ChildCount for Spotify playlists in the response to show total tracks (local + matched). /// @@ -2184,7 +2428,7 @@ public class JellyfinController : ControllerBase } // Get existing Jellyfin playlist items (tracks the plugin already found) - var existingTracksResponse = await _proxyService.GetJsonAsync( + var (existingTracksResponse, _) = await _proxyService.GetJsonAsync( $"Playlists/{playlistId}/Items", null, Request.Headers); diff --git a/allstarr/Services/Jellyfin/JellyfinProxyService.cs b/allstarr/Services/Jellyfin/JellyfinProxyService.cs index 4c9b6dc..43f4fcd 100644 --- a/allstarr/Services/Jellyfin/JellyfinProxyService.cs +++ b/allstarr/Services/Jellyfin/JellyfinProxyService.cs @@ -103,8 +103,9 @@ public class JellyfinProxyService /// /// Sends a GET request to the Jellyfin server. /// If endpoint already contains query parameters, they will be preserved and merged with queryParams. + /// Returns the response body and HTTP status code. /// - public async Task GetJsonAsync(string endpoint, Dictionary? queryParams = null, IHeaderDictionary? clientHeaders = null) + public async Task<(JsonDocument? Body, int StatusCode)> GetJsonAsync(string endpoint, Dictionary? queryParams = null, IHeaderDictionary? clientHeaders = null) { // If endpoint contains query string, parse and merge with queryParams if (endpoint.Contains('?')) @@ -141,7 +142,7 @@ public class JellyfinProxyService return await GetJsonAsyncInternal(finalUrl, clientHeaders); } - private async Task GetJsonAsyncInternal(string url, IHeaderDictionary? clientHeaders) + private async Task<(JsonDocument? Body, int StatusCode)> GetJsonAsyncInternal(string url, IHeaderDictionary? clientHeaders) { using var request = new HttpRequestMessage(HttpMethod.Get, url); @@ -216,6 +217,8 @@ public class JellyfinProxyService var response = await _httpClient.SendAsync(request); + var statusCode = (int)response.StatusCode; + // Always parse the response, even for errors // The caller needs to see 401s so the client can re-authenticate var content = await response.Content.ReadAsStringAsync(); @@ -231,19 +234,19 @@ public class JellyfinProxyService _logger.LogWarning("Jellyfin request failed: {StatusCode} for {Url}", response.StatusCode, url); } - // Return null so caller knows request failed - // TODO: We should return the status code too so caller can pass it through - return null; + // Return null body with the actual status code + return (null, statusCode); } - return JsonDocument.Parse(content); + return (JsonDocument.Parse(content), statusCode); } /// /// Sends a POST request to the Jellyfin server with JSON body. /// Forwards client headers for authentication passthrough. + /// Returns the response body and HTTP status code. /// - public async Task PostJsonAsync(string endpoint, string body, IHeaderDictionary clientHeaders) + public async Task<(JsonDocument? Body, int StatusCode)> PostJsonAsync(string endpoint, string body, IHeaderDictionary clientHeaders) { var url = BuildUrl(endpoint, null); @@ -354,18 +357,20 @@ public class JellyfinProxyService var response = await _httpClient.SendAsync(request); + var statusCode = (int)response.StatusCode; + if (!response.IsSuccessStatusCode) { var errorContent = await response.Content.ReadAsStringAsync(); _logger.LogWarning("Jellyfin POST request failed: {StatusCode} for {Url}. Response: {Response}", response.StatusCode, url, errorContent); - return null; + return (null, statusCode); } // Handle 204 No Content responses (e.g., /sessions/playing, /sessions/playing/progress) if (response.StatusCode == System.Net.HttpStatusCode.NoContent) { - return null; + return (null, statusCode); } var responseContent = await response.Content.ReadAsStringAsync(); @@ -373,10 +378,10 @@ public class JellyfinProxyService // Handle empty responses if (string.IsNullOrWhiteSpace(responseContent)) { - return null; + return (null, statusCode); } - return JsonDocument.Parse(responseContent); + return (JsonDocument.Parse(responseContent), statusCode); } /// @@ -401,8 +406,9 @@ public class JellyfinProxyService /// /// Sends a DELETE request to the Jellyfin server. /// Forwards client headers for authentication passthrough. + /// Returns the response body and HTTP status code. /// - public async Task DeleteAsync(string endpoint, IHeaderDictionary clientHeaders) + public async Task<(JsonDocument? Body, int StatusCode)> DeleteAsync(string endpoint, IHeaderDictionary clientHeaders) { var url = BuildUrl(endpoint, null); @@ -462,18 +468,20 @@ public class JellyfinProxyService var response = await _httpClient.SendAsync(request); + var statusCode = (int)response.StatusCode; + if (!response.IsSuccessStatusCode) { var errorContent = await response.Content.ReadAsStringAsync(); _logger.LogWarning("Jellyfin DELETE request failed: {StatusCode} for {Url}. Response: {Response}", response.StatusCode, url, errorContent); - return null; + return (null, statusCode); } // Handle 204 No Content responses if (response.StatusCode == System.Net.HttpStatusCode.NoContent) { - return null; + return (null, statusCode); } var responseContent = await response.Content.ReadAsStringAsync(); @@ -481,10 +489,10 @@ public class JellyfinProxyService // Handle empty responses if (string.IsNullOrWhiteSpace(responseContent)) { - return null; + return (null, statusCode); } - return JsonDocument.Parse(responseContent); + return (JsonDocument.Parse(responseContent), statusCode); } /// @@ -510,7 +518,7 @@ public class JellyfinProxyService /// Searches for items in Jellyfin. /// Uses configured or auto-detected LibraryId to filter search to music library only. /// - public async Task SearchAsync( + public async Task<(JsonDocument? Body, int StatusCode)> SearchAsync( string searchTerm, string[]? includeItemTypes = null, int limit = 20, @@ -548,7 +556,7 @@ public class JellyfinProxyService /// /// Gets items from a specific parent (album, artist, playlist). /// - public async Task GetItemsAsync( + public async Task<(JsonDocument? Body, int StatusCode)> GetItemsAsync( string? parentId = null, string[]? includeItemTypes = null, string? sortBy = null, @@ -604,7 +612,7 @@ public class JellyfinProxyService /// /// Gets a single item by ID. /// - public async Task GetItemAsync(string itemId, IHeaderDictionary? clientHeaders = null) + public async Task<(JsonDocument? Body, int StatusCode)> GetItemAsync(string itemId, IHeaderDictionary? clientHeaders = null) { var queryParams = new Dictionary(); @@ -619,7 +627,7 @@ public class JellyfinProxyService /// /// Gets artists from the library. /// - public async Task GetArtistsAsync( + public async Task<(JsonDocument? Body, int StatusCode)> GetArtistsAsync( string? searchTerm = null, int? limit = null, int? startIndex = null, @@ -656,7 +664,7 @@ public class JellyfinProxyService /// /// Gets an artist by name or ID. /// - public async Task GetArtistAsync(string artistIdOrName, IHeaderDictionary? clientHeaders = null) + public async Task<(JsonDocument? Body, int StatusCode)> GetArtistAsync(string artistIdOrName, IHeaderDictionary? clientHeaders = null) { var queryParams = new Dictionary(); @@ -817,8 +825,8 @@ public class JellyfinProxyService { try { - var result = await GetJsonAsync("System/Info/Public"); - if (result == null) + var (result, statusCode) = await GetJsonAsync("System/Info/Public"); + if (result == null || statusCode != 200) { return (false, null, null); } @@ -852,7 +860,7 @@ public class JellyfinProxyService queryParams["userId"] = _settings.UserId; } - var result = await GetJsonAsync("Library/MediaFolders", queryParams); + var (result, statusCode) = await GetJsonAsync("Library/MediaFolders", queryParams); if (result == null) { return null;