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