diff --git a/allstarr/Controllers/JellyfinController.cs b/allstarr/Controllers/JellyfinController.cs index 37f91b7..70c8a77 100644 --- a/allstarr/Controllers/JellyfinController.cs +++ b/allstarr/Controllers/JellyfinController.cs @@ -1731,6 +1731,7 @@ public class JellyfinController : ControllerBase /// /// Reports playback start. Handles both local and external tracks. /// For local tracks, forwards to Jellyfin. For external tracks, logs locally. + /// Also ensures session is initialized if this is the first report from a device. /// [HttpPost("Sessions/Playing")] public async Task ReportPlaybackStart() @@ -1749,6 +1750,50 @@ public class JellyfinController : ControllerBase _logger.LogInformation("Auth headers: {Headers}", string.Join(", ", Request.Headers.Where(h => h.Key.Contains("Auth", StringComparison.OrdinalIgnoreCase)).Select(h => $"{h.Key}={h.Value}"))); + // Extract device info from auth headers for session initialization + string? deviceId = null; + string? client = null; + string? device = null; + string? version = null; + + if (Request.Headers.TryGetValue("Authorization", out var authHeader)) + { + var authStr = authHeader.ToString(); + // Parse: MediaBrowser Client="...", Device="...", DeviceId="...", Version="..." + var parts = authStr.Replace("MediaBrowser ", "").Split(','); + foreach (var part in parts) + { + var kv = part.Trim().Split('='); + if (kv.Length == 2) + { + var key = kv[0].Trim(); + var value = kv[1].Trim('"'); + if (key == "DeviceId") deviceId = value; + else if (key == "Client") client = value; + else if (key == "Device") device = value; + else if (key == "Version") version = value; + } + } + } + + // Ensure session capabilities are posted to Jellyfin (if not already done) + if (!string.IsNullOrEmpty(deviceId)) + { + _logger.LogInformation("🔧 Ensuring session exists for device: {DeviceId} ({Client} {Version})", deviceId, client, version); + + // Post capabilities to ensure session is created + var capabilitiesEndpoint = $"Sessions/Capabilities?playableMediaTypes=Audio&supportedCommands=&supportsMediaControl=false&supportsPersistentIdentifier=true"; + try + { + var (capResult, capStatus) = await _proxyService.PostJsonAsync(capabilitiesEndpoint, "{}", Request.Headers); + _logger.LogInformation("✓ Session capabilities posted ({StatusCode})", capStatus); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to post session capabilities, continuing anyway"); + } + } + // Parse the body to check if it's an external track var doc = JsonDocument.Parse(body); string? itemId = null; @@ -1945,6 +1990,60 @@ public class JellyfinController : ControllerBase } } + /// + /// Catch-all for any other session-related requests. + /// This ensures all session management calls get proxied to Jellyfin. + /// + [HttpGet("Sessions/{**path}")] + [HttpPost("Sessions/{**path}")] + [HttpPut("Sessions/{**path}")] + [HttpDelete("Sessions/{**path}")] + public async Task ProxySessionRequest(string path) + { + try + { + var method = Request.Method; + var queryString = Request.QueryString.HasValue ? Request.QueryString.Value : ""; + var endpoint = $"Sessions/{path}{queryString}"; + + _logger.LogInformation("🔄 Proxying session request: {Method} {Endpoint}", method, endpoint); + + // Read body if present + string body = "{}"; + if ((method == "POST" || method == "PUT") && Request.ContentLength > 0) + { + Request.EnableBuffering(); + 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; + } + + // Forward to Jellyfin + var (result, statusCode) = method switch + { + "GET" => await _proxyService.GetJsonAsync(endpoint, null, Request.Headers), + "POST" => await _proxyService.PostJsonAsync(endpoint, body, Request.Headers), + "PUT" => await _proxyService.PostJsonAsync(endpoint, body, Request.Headers), // Use POST for PUT + "DELETE" => await _proxyService.PostJsonAsync(endpoint, body, Request.Headers), // Use POST for DELETE + _ => (null, 405) + }; + + if (result != null) + { + return new JsonResult(result.RootElement.Clone()); + } + + return StatusCode(statusCode); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to proxy session request"); + return StatusCode(500); + } + } + #endregion // Session Management #endregion // Playback Session Reporting @@ -1989,8 +2088,16 @@ public class JellyfinController : ControllerBase [HttpPost("{**path}", Order = 100)] public async Task ProxyRequest(string path) { - // DEBUG: Log EVERY request to see what's happening - _logger.LogWarning("ProxyRequest called with path: {Path}", path); + // Log session-related requests prominently to debug missing capabilities call + if (path.Contains("session", StringComparison.OrdinalIgnoreCase) || + path.Contains("capabilit", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogWarning("🔍 SESSION/CAPABILITY REQUEST: {Method} /{Path}{Query}", Request.Method, path, Request.QueryString); + } + else + { + _logger.LogDebug("ProxyRequest: {Method} /{Path}", Request.Method, path); + } // Log endpoint usage to file for analysis await LogEndpointUsageAsync(path, Request.Method); @@ -2216,7 +2323,8 @@ public class JellyfinController : ControllerBase result = await UpdateSpotifyPlaylistCounts(result); } - return new JsonResult(JsonSerializer.Deserialize(result.RootElement.GetRawText())); + // Return the raw JSON element directly to avoid deserialization issues with simple types + return new JsonResult(result.RootElement.Clone()); } catch (Exception ex) {