diff --git a/allstarr/Controllers/JellyfinController.cs b/allstarr/Controllers/JellyfinController.cs index b99d5bf..3c3b4bd 100644 --- a/allstarr/Controllers/JellyfinController.cs +++ b/allstarr/Controllers/JellyfinController.cs @@ -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 /// [HttpGet("Items/{itemId}/Similar")] [HttpGet("Songs/{itemId}/Similar")] - [HttpGet("Artists/{artistId}/Similar")] + [HttpGet("Artists/{itemId}/Similar")] public async Task 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 { ["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); } } diff --git a/allstarr/Controllers/SubSonicController.cs b/allstarr/Controllers/SubSonicController.cs index 97183d4..19cf510 100644 --- a/allstarr/Controllers/SubSonicController.cs +++ b/allstarr/Controllers/SubSonicController.cs @@ -64,7 +64,7 @@ public class SubsonicController : ControllerBase { return await _requestParser.ExtractAllParametersAsync(Request); } - + /// /// Merges local and external search results. /// @@ -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); } diff --git a/allstarr/Program.cs b/allstarr/Program.cs index 8013223..382a199 100644 --- a/allstarr/Program.cs +++ b/allstarr/Program.cs @@ -71,6 +71,8 @@ builder.Services.Configure( builder.Configuration.GetSection("Deezer")); builder.Services.Configure( builder.Configuration.GetSection("Qobuz")); +builder.Services.Configure( + builder.Configuration.GetSection("SquidWTF")); builder.Services.Configure( builder.Configuration.GetSection("Redis")); diff --git a/allstarr/Services/Jellyfin/JellyfinProxyService.cs b/allstarr/Services/Jellyfin/JellyfinProxyService.cs index 447ffdb..4cbeba3 100644 --- a/allstarr/Services/Jellyfin/JellyfinProxyService.cs +++ b/allstarr/Services/Jellyfin/JellyfinProxyService.cs @@ -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); } } diff --git a/allstarr/Services/Jellyfin/JellyfinResponseBuilder.cs b/allstarr/Services/Jellyfin/JellyfinResponseBuilder.cs index 7d67a06..93acc4e 100644 --- a/allstarr/Services/Jellyfin/JellyfinResponseBuilder.cs +++ b/allstarr/Services/Jellyfin/JellyfinResponseBuilder.cs @@ -304,10 +304,17 @@ public class JellyfinResponseBuilder /// public Dictionary 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 { ["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(), - ["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 { ["PlaybackPositionTicks"] = 0, @@ -364,10 +371,17 @@ public class JellyfinResponseBuilder /// public Dictionary 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 { ["Id"] = artist.Id, - ["Name"] = artist.Name, + ["Name"] = artistName, ["ServerId"] = "allstarr", ["Type"] = "MusicArtist", ["IsFolder"] = true, diff --git a/allstarr/Services/SquidWTF/SquidWTFDownloadService.cs b/allstarr/Services/SquidWTF/SquidWTFDownloadService.cs index ec30a86..1b3d325 100644 --- a/allstarr/Services/SquidWTF/SquidWTFDownloadService.cs +++ b/allstarr/Services/SquidWTF/SquidWTFDownloadService.cs @@ -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;