Fix multiple Jellyfin proxy issues and improvements

- Fix playback progress reporting by wrapping POST bodies in required field names
- Fix cache cleanup by updating last access time when streaming files
- Fix Artists/{id}/Similar endpoint proxying to correct Jellyfin endpoint
- Add ' - SW' suffix to external albums and artists for better identification
- Register SquidWTFSettings configuration to enable quality settings
- Remove unused code and improve debugging logs
This commit is contained in:
2026-01-30 01:58:10 -05:00
parent 457a5b7582
commit aebb1c14dd
6 changed files with 89 additions and 23 deletions

View File

@@ -811,6 +811,16 @@ public class JellyfinController : ControllerBase
if (localPath != null && System.IO.File.Exists(localPath))
{
// Update last access time for cache cleanup
try
{
System.IO.File.SetLastAccessTimeUtc(localPath, DateTime.UtcNow);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to update last access time for {Path}", localPath);
}
var stream = System.IO.File.OpenRead(localPath);
return File(stream, GetContentType(localPath), enableRangeProcessing: true);
}
@@ -1202,7 +1212,7 @@ public class JellyfinController : ControllerBase
/// </summary>
[HttpGet("Items/{itemId}/Similar")]
[HttpGet("Songs/{itemId}/Similar")]
[HttpGet("Artists/{artistId}/Similar")]
[HttpGet("Artists/{itemId}/Similar")]
public async Task<IActionResult> GetSimilarItems(
string itemId,
[FromQuery] int limit = 50,
@@ -1266,7 +1276,11 @@ public class JellyfinController : ControllerBase
}
}
// For local items, proxy to Jellyfin
// For local items, determine the correct endpoint based on the request path
var endpoint = Request.Path.Value?.Contains("/Artists/", StringComparison.OrdinalIgnoreCase) == true
? $"Artists/{itemId}/Similar"
: $"Items/{itemId}/Similar";
var queryParams = new Dictionary<string, string>
{
["limit"] = limit.ToString()
@@ -1282,7 +1296,7 @@ public class JellyfinController : ControllerBase
queryParams["userId"] = userId;
}
var result = await _proxyService.GetJsonAsync($"Items/{itemId}/Similar", queryParams, Request.Headers);
var result = await _proxyService.GetJsonAsync(endpoint, queryParams, Request.Headers);
if (result == null)
{
@@ -1532,28 +1546,32 @@ public class JellyfinController : ControllerBase
// Read body using StreamReader with proper encoding
string body;
using (var reader = new StreamReader(Request.Body, System.Text.Encoding.UTF8, leaveOpen: true))
using (var reader = new StreamReader(Request.Body, System.Text.Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 1024, leaveOpen: true))
{
body = await reader.ReadToEndAsync();
}
// Reset stream position after reading
// Reset stream position after reading so it can be read again if needed
Request.Body.Position = 0;
if (string.IsNullOrWhiteSpace(body))
{
_logger.LogWarning("Empty POST body for {Path}, ContentLength={ContentLength}, ContentType={ContentType}",
_logger.LogWarning("Empty POST body received from client for {Path}, ContentLength={ContentLength}, ContentType={ContentType}",
fullPath, Request.ContentLength, Request.ContentType);
// Log all headers to debug
_logger.LogWarning("Request headers: {Headers}",
string.Join(", ", Request.Headers.Select(h => $"{h.Key}={h.Value}")));
}
else
{
_logger.LogInformation("POST body for {Path}: {BodyLength} bytes, ContentType={ContentType}",
_logger.LogInformation("POST body received from client for {Path}: {BodyLength} bytes, ContentType={ContentType}",
fullPath, body.Length, Request.ContentType);
// Always log body content for playback endpoints to debug the issue
if (fullPath.Contains("Playing", StringComparison.OrdinalIgnoreCase))
{
_logger.LogInformation("POST body content: {Body}", body);
_logger.LogInformation("POST body content from client: {Body}", body);
}
}

View File

@@ -64,7 +64,7 @@ public class SubsonicController : ControllerBase
{
return await _requestParser.ExtractAllParametersAsync(Request);
}
/// <summary>
/// Merges local and external search results.
/// </summary>
@@ -142,6 +142,16 @@ public class SubsonicController : ControllerBase
if (localPath != null && System.IO.File.Exists(localPath))
{
// Update last access time for cache cleanup
try
{
System.IO.File.SetLastAccessTimeUtc(localPath, DateTime.UtcNow);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to update last access time for {Path}", localPath);
}
var stream = System.IO.File.OpenRead(localPath);
return File(stream, GetContentType(localPath), enableRangeProcessing: true);
}

View File

@@ -71,6 +71,8 @@ builder.Services.Configure<DeezerSettings>(
builder.Configuration.GetSection("Deezer"));
builder.Services.Configure<QobuzSettings>(
builder.Configuration.GetSection("Qobuz"));
builder.Services.Configure<SquidWTFSettings>(
builder.Configuration.GetSection("SquidWTF"));
builder.Services.Configure<RedisSettings>(
builder.Configuration.GetSection("Redis"));

View File

@@ -256,17 +256,40 @@ public class JellyfinProxyService
using var request = new HttpRequestMessage(HttpMethod.Post, url);
// Create content from body string
if (!string.IsNullOrEmpty(body))
// Handle special case for playback endpoints - Jellyfin expects wrapped body
var bodyToSend = body;
if (!string.IsNullOrWhiteSpace(body))
{
request.Content = new StringContent(body, System.Text.Encoding.UTF8, "application/json");
_logger.LogDebug("POST body length: {Length} bytes", body.Length);
// Check if this is a playback progress endpoint
if (endpoint.Contains("Sessions/Playing/Progress", StringComparison.OrdinalIgnoreCase))
{
// Wrap the body in playbackProgressInfo field
bodyToSend = $"{{\"playbackProgressInfo\":{body}}}";
_logger.LogDebug("Wrapped body for playback progress endpoint");
}
else if (endpoint.Contains("Sessions/Playing/Stopped", StringComparison.OrdinalIgnoreCase))
{
// Wrap the body in playbackStopInfo field
bodyToSend = $"{{\"playbackStopInfo\":{body}}}";
_logger.LogDebug("Wrapped body for playback stopped endpoint");
}
else if (endpoint.Contains("Sessions/Playing", StringComparison.OrdinalIgnoreCase) &&
!endpoint.Contains("Progress", StringComparison.OrdinalIgnoreCase) &&
!endpoint.Contains("Stopped", StringComparison.OrdinalIgnoreCase))
{
// Wrap the body in playbackStartInfo field for /Sessions/Playing
bodyToSend = $"{{\"playbackStartInfo\":{body}}}";
_logger.LogDebug("Wrapped body for playback start endpoint");
}
}
else
{
_logger.LogWarning("POST body is empty for {Url}", url);
bodyToSend = "{}";
_logger.LogWarning("POST body was empty for {Url}, sending empty JSON object", url);
}
request.Content = new StringContent(bodyToSend, System.Text.Encoding.UTF8, "application/json");
bool authHeaderAdded = false;
// Forward authentication headers from client (case-insensitive)
@@ -312,12 +335,12 @@ public class JellyfinProxyService
}
else
{
_logger.LogInformation("POST to Jellyfin: {Url}, body length: {Length} bytes", url, body.Length);
_logger.LogInformation("POST to Jellyfin: {Url}, body length: {Length} bytes", url, bodyToSend.Length);
// Log body content for playback endpoints to debug
if (endpoint.Contains("Playing", StringComparison.OrdinalIgnoreCase))
{
_logger.LogInformation("Sending body to Jellyfin: {Body}", body);
_logger.LogInformation("Sending body to Jellyfin: {Body}", bodyToSend);
}
}

View File

@@ -304,10 +304,17 @@ public class JellyfinResponseBuilder
/// </summary>
public Dictionary<string, object?> ConvertAlbumToJellyfinItem(Album album)
{
// Add " - SW" suffix to external album names
var albumName = album.Title;
if (!album.IsLocal)
{
albumName = $"{album.Title} - SW";
}
var item = new Dictionary<string, object?>
{
["Id"] = album.Id,
["Name"] = album.Title,
["Name"] = albumName,
["ServerId"] = "allstarr",
["Type"] = "MusicAlbum",
["IsFolder"] = true,
@@ -328,10 +335,10 @@ public class JellyfinResponseBuilder
},
["BackdropImageTags"] = new string[0],
["ImageBlurHashes"] = new Dictionary<string, object>(),
["LocationType"] = "FileSystem", // External content appears as local files to clients
["MediaType"] = (object?)null, // Match Jellyfin structure
["ChannelId"] = (object?)null, // Match Jellyfin structure
["CollectionType"] = (object?)null, // Match Jellyfin structure
["LocationType"] = "FileSystem",
["MediaType"] = (object?)null,
["ChannelId"] = (object?)null,
["CollectionType"] = (object?)null,
["UserData"] = new Dictionary<string, object>
{
["PlaybackPositionTicks"] = 0,
@@ -364,10 +371,17 @@ public class JellyfinResponseBuilder
/// </summary>
public Dictionary<string, object?> ConvertArtistToJellyfinItem(Artist artist)
{
// Add " - SW" suffix to external artist names
var artistName = artist.Name;
if (!artist.IsLocal)
{
artistName = $"{artist.Name} - SW";
}
var item = new Dictionary<string, object?>
{
["Id"] = artist.Id,
["Name"] = artist.Name,
["Name"] = artistName,
["ServerId"] = "allstarr",
["Type"] = "MusicArtist",
["IsFolder"] = true,

View File

@@ -21,7 +21,6 @@ public class SquidWTFDownloadService : BaseDownloadService
{
private readonly HttpClient _httpClient;
private readonly SemaphoreSlim _requestLock = new(1, 1);
private readonly string? _preferredQuality;
private readonly SquidWTFSettings _squidwtfSettings;
private DateTime _lastRequestTime = DateTime.MinValue;