From 349fb740a2451cec33da3958eb9f48f3bcda0e2d Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Thu, 5 Feb 2026 11:56:26 -0500 Subject: [PATCH] Fix scrobbling: track playing item in session and send proper PlaybackStopped data on cleanup --- allstarr/Controllers/JellyfinController.cs | 22 ++++++++++-- .../Jellyfin/JellyfinSessionManager.cs | 34 ++++++++++++++++--- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/allstarr/Controllers/JellyfinController.cs b/allstarr/Controllers/JellyfinController.cs index dc91dd9..4e1c0a3 100644 --- a/allstarr/Controllers/JellyfinController.cs +++ b/allstarr/Controllers/JellyfinController.cs @@ -2006,6 +2006,7 @@ public class JellyfinController : ControllerBase var doc = JsonDocument.Parse(body); string? itemId = null; string? itemName = null; + long? positionTicks = null; if (doc.RootElement.TryGetProperty("ItemId", out var itemIdProp)) { @@ -2016,6 +2017,18 @@ public class JellyfinController : ControllerBase { itemName = itemNameProp.GetString(); } + + if (doc.RootElement.TryGetProperty("PositionTicks", out var posProp)) + { + positionTicks = posProp.GetInt64(); + } + + // Track the playing item for scrobbling on session cleanup + var (deviceId, client, device, version) = ExtractDeviceInfo(Request.Headers); + if (!string.IsNullOrEmpty(deviceId) && !string.IsNullOrEmpty(itemId)) + { + _sessionManager.UpdatePlayingItem(deviceId, itemId, positionTicks); + } if (!string.IsNullOrEmpty(itemId)) { @@ -2050,7 +2063,7 @@ public class JellyfinController : ControllerBase var playbackStart = new { ItemId = itemId, - PositionTicks = doc.RootElement.TryGetProperty("PositionTicks", out var posProp) ? posProp.GetInt64() : 0, + PositionTicks = positionTicks ?? 0, // Let Jellyfin fetch the item details - don't include NowPlayingItem }; @@ -2064,7 +2077,6 @@ public class JellyfinController : ControllerBase _logger.LogInformation("✓ Playback start forwarded to Jellyfin ({StatusCode})", statusCode); // NOW ensure session exists with capabilities (after playback is reported) - var (deviceId, client, device, version) = ExtractDeviceInfo(Request.Headers); if (!string.IsNullOrEmpty(deviceId)) { var sessionCreated = await _sessionManager.EnsureSessionAsync(deviceId, client ?? "Unknown", device ?? "Unknown", version ?? "1.0", Request.Headers); @@ -2155,6 +2167,12 @@ public class JellyfinController : ControllerBase positionTicks = posProp.GetInt64(); } + // Track the playing item for scrobbling on session cleanup + if (!string.IsNullOrEmpty(deviceId) && !string.IsNullOrEmpty(itemId)) + { + _sessionManager.UpdatePlayingItem(deviceId, itemId, positionTicks); + } + if (!string.IsNullOrEmpty(itemId)) { var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(itemId); diff --git a/allstarr/Services/Jellyfin/JellyfinSessionManager.cs b/allstarr/Services/Jellyfin/JellyfinSessionManager.cs index 382947b..b900e35 100644 --- a/allstarr/Services/Jellyfin/JellyfinSessionManager.cs +++ b/allstarr/Services/Jellyfin/JellyfinSessionManager.cs @@ -142,6 +142,21 @@ public class JellyfinSessionManager : IDisposable _logger.LogDebug("⚠️ SESSION: Cannot update activity - device {DeviceId} not found", deviceId); } } + + /// + /// Updates the currently playing item for a session (for scrobbling on cleanup). + /// + public void UpdatePlayingItem(string deviceId, string? itemId, long? positionTicks) + { + if (_sessions.TryGetValue(deviceId, out var session)) + { + session.LastPlayingItemId = itemId; + session.LastPlayingPositionTicks = positionTicks; + session.LastActivity = DateTime.UtcNow; + _logger.LogDebug("🎵 SESSION: Updated playing item for {DeviceId}: {ItemId} at {Position}", + deviceId, itemId, positionTicks); + } + } /// /// Marks a session as potentially ended (e.g., after playback stops). @@ -230,10 +245,19 @@ public class JellyfinSessionManager : IDisposable try { - // Report playback stopped to Jellyfin - var stopPayload = JsonSerializer.Serialize(new { }); - await _proxyService.PostJsonAsync("Sessions/Playing/Stopped", stopPayload, session.Headers); - _logger.LogDebug("🛑 SESSION: Reported playback stopped for {DeviceId}", deviceId); + // Report playback stopped to Jellyfin if we have a playing item (for scrobbling) + if (!string.IsNullOrEmpty(session.LastPlayingItemId)) + { + var stopPayload = new + { + ItemId = session.LastPlayingItemId, + PositionTicks = session.LastPlayingPositionTicks ?? 0 + }; + var stopJson = JsonSerializer.Serialize(stopPayload); + await _proxyService.PostJsonAsync("Sessions/Playing/Stopped", stopJson, session.Headers); + _logger.LogInformation("🛑 SESSION: Reported playback stopped for {DeviceId} (ItemId: {ItemId}, Position: {Position})", + deviceId, session.LastPlayingItemId, session.LastPlayingPositionTicks); + } // Notify Jellyfin that the session is ending await _proxyService.PostJsonAsync("Sessions/Logout", "{}", session.Headers); @@ -500,6 +524,8 @@ public class JellyfinSessionManager : IDisposable public DateTime LastActivity { get; set; } public required IHeaderDictionary Headers { get; init; } public ClientWebSocket? WebSocket { get; set; } + public string? LastPlayingItemId { get; set; } + public long? LastPlayingPositionTicks { get; set; } } public void Dispose()