Fix: Properly handle HTTP status codes in Jellyfin proxy

- 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
This commit is contained in:
2026-02-02 01:05:25 -05:00
parent 8e6eb5cc4a
commit 65ca80f9a0
2 changed files with 368 additions and 116 deletions

View File

@@ -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<object>(), 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<object>(result.RootElement.GetRawText()));
return HandleProxyResponse(result, statusCode);
}
/// <summary>
@@ -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");
}
var (result, statusCode) = await _proxyService.GetItemAsync(itemId, Request.Headers);
return new JsonResult(JsonSerializer.Deserialize<object>(result.RootElement.GetRawText()));
return HandleProxyResponse(result, statusCode);
}
/// <summary>
@@ -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<string, object>
{
["Items"] = Array.Empty<object>(),
["TotalRecordCount"] = 0,
["StartIndex"] = startIndex
});
}
return new JsonResult(JsonSerializer.Deserialize<object>(result.RootElement.GetRawText()));
Items = Array.Empty<object>(),
TotalRecordCount = 0,
StartIndex = startIndex
});
}
/// <summary>
@@ -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<object>(result.RootElement.GetRawText()));
return HandleProxyResponse(result, statusCode);
}
/// <summary>
@@ -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)
return HandleProxyResponse(result, statusCode, new
{
// 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<object>(result.RootElement.GetRawText()));
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<object>(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<object>(),
TotalRecordCount = 0
});
}
return new JsonResult(JsonSerializer.Deserialize<object>(result.RootElement.GetRawText()));
return HandleProxyResponse(result, statusCode, new { Items = Array.Empty<object>(), TotalRecordCount = 0 });
}
/// <summary>
@@ -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 HandleProxyResponse(result, statusCode, new { Items = Array.Empty<object>(), TotalRecordCount = 0 });
}
#endregion
#region Playback Session Reporting
/// <summary>
/// Reports playback start. Handles both local and external tracks.
/// For local tracks, forwards to Jellyfin. For external tracks, logs locally.
/// </summary>
[HttpPost("Sessions/Playing")]
public async Task<IActionResult> ReportPlaybackStart()
{
try
{
return _responseBuilder.CreateJsonResponse(new
Request.EnableBuffering();
string body;
using (var reader = new StreamReader(Request.Body, System.Text.Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 1024, leaveOpen: true))
{
Items = Array.Empty<object>(),
TotalRecordCount = 0
});
}
body = await reader.ReadToEndAsync();
}
Request.Body.Position = 0;
return new JsonResult(JsonSerializer.Deserialize<object>(result.RootElement.GetRawText()));
_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
}
}
/// <summary>
/// Reports playback progress. Handles both local and external tracks.
/// </summary>
[HttpPost("Sessions/Playing/Progress")]
public async Task<IActionResult> 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();
}
}
/// <summary>
/// Reports playback stopped. Handles both local and external tracks.
/// </summary>
[HttpPost("Sessions/Playing/Stopped")]
public async Task<IActionResult> 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();
}
}
/// <summary>
/// Pings a playback session to keep it alive.
/// </summary>
[HttpPost("Sessions/Playing/Ping")]
public async Task<IActionResult> 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
/// <summary>
/// Helper to handle proxy responses with proper status code handling.
/// </summary>
private IActionResult HandleProxyResponse(JsonDocument? result, int statusCode, object? fallbackValue = null)
{
if (result != null)
{
return new JsonResult(JsonSerializer.Deserialize<object>(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();
}
/// <summary>
/// Updates ChildCount for Spotify playlists in the response to show total tracks (local + matched).
/// </summary>
@@ -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);

View File

@@ -103,8 +103,9 @@ public class JellyfinProxyService
/// <summary>
/// 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.
/// </summary>
public async Task<JsonDocument?> GetJsonAsync(string endpoint, Dictionary<string, string>? queryParams = null, IHeaderDictionary? clientHeaders = null)
public async Task<(JsonDocument? Body, int StatusCode)> GetJsonAsync(string endpoint, Dictionary<string, string>? 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<JsonDocument?> 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);
}
/// <summary>
/// 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.
/// </summary>
public async Task<JsonDocument?> 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);
}
/// <summary>
@@ -401,8 +406,9 @@ public class JellyfinProxyService
/// <summary>
/// Sends a DELETE request to the Jellyfin server.
/// Forwards client headers for authentication passthrough.
/// Returns the response body and HTTP status code.
/// </summary>
public async Task<JsonDocument?> 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);
}
/// <summary>
@@ -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.
/// </summary>
public async Task<JsonDocument?> SearchAsync(
public async Task<(JsonDocument? Body, int StatusCode)> SearchAsync(
string searchTerm,
string[]? includeItemTypes = null,
int limit = 20,
@@ -548,7 +556,7 @@ public class JellyfinProxyService
/// <summary>
/// Gets items from a specific parent (album, artist, playlist).
/// </summary>
public async Task<JsonDocument?> 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
/// <summary>
/// Gets a single item by ID.
/// </summary>
public async Task<JsonDocument?> GetItemAsync(string itemId, IHeaderDictionary? clientHeaders = null)
public async Task<(JsonDocument? Body, int StatusCode)> GetItemAsync(string itemId, IHeaderDictionary? clientHeaders = null)
{
var queryParams = new Dictionary<string, string>();
@@ -619,7 +627,7 @@ public class JellyfinProxyService
/// <summary>
/// Gets artists from the library.
/// </summary>
public async Task<JsonDocument?> 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
/// <summary>
/// Gets an artist by name or ID.
/// </summary>
public async Task<JsonDocument?> GetArtistAsync(string artistIdOrName, IHeaderDictionary? clientHeaders = null)
public async Task<(JsonDocument? Body, int StatusCode)> GetArtistAsync(string artistIdOrName, IHeaderDictionary? clientHeaders = null)
{
var queryParams = new Dictionary<string, string>();
@@ -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;