Fix RuntimeBinderException, add session cleanup, memory stats endpoint, and fix all warnings

- Fixed RuntimeBinderException when comparing JsonElement with null
- Added HasValue() helper method for safe dynamic type checking
- Implemented intelligent session cleanup:
  * 50 seconds after playback stops (allows song changes)
  * 3 minutes of total inactivity (catches crashed clients)
- Added memory stats endpoint: GET /api/admin/memory-stats
- Added sessions monitoring endpoint: GET /api/admin/sessions
- Added GetSessionsInfo() to JellyfinSessionManager for debugging
- Fixed all nullable reference warnings
- Reduced warnings from 10 to 0
This commit is contained in:
2026-02-05 09:17:40 -05:00
parent d9c0b8bb54
commit 3fd13b855d
5 changed files with 136 additions and 16 deletions

View File

@@ -82,6 +82,17 @@ public class AdminController : ControllerBase
_logger.LogInformation("Admin controller initialized. .env path: {EnvFilePath}", _envFilePath);
}
/// <summary>
/// Helper method to safely check if a dynamic cache result has a value
/// Handles the case where JsonElement cannot be compared to null directly
/// </summary>
private static bool HasValue(dynamic? obj)
{
if (obj == null) return false;
if (obj is JsonElement jsonEl) return jsonEl.ValueKind != JsonValueKind.Null;
return true;
}
/// <summary>
/// Get current system status and configuration
/// </summary>
@@ -522,12 +533,12 @@ public class AdminController : ControllerBase
var externalMappingKey = $"spotify:external-map:{decodedName}:{track.SpotifyId}";
var externalMapping = await _cache.GetAsync<dynamic>(externalMappingKey);
if (externalMapping != null)
if (HasValue(externalMapping))
{
try
{
var provider = externalMapping.provider?.ToString();
var externalId = externalMapping.id?.ToString();
var provider = externalMapping?.provider?.ToString();
var externalId = externalMapping?.id?.ToString();
if (!string.IsNullOrEmpty(provider) && !string.IsNullOrEmpty(externalId))
{
@@ -535,7 +546,7 @@ public class AdminController : ControllerBase
isLocal = false;
externalProvider = provider;
_logger.LogDebug("✓ Manual external mapping found for {Title}: {Provider} {ExternalId}",
track.Title, (object)provider, (object)externalId);
track.Title, (object?)provider, (object?)externalId);
}
}
catch (Exception ex)
@@ -646,11 +657,11 @@ public class AdminController : ControllerBase
var externalMappingKey = $"spotify:external-map:{decodedName}:{track.SpotifyId}";
var externalMapping = await _cache.GetAsync<dynamic>(externalMappingKey);
if (externalMapping != null)
if (HasValue(externalMapping))
{
try
{
var provider = externalMapping.provider?.ToString();
var provider = externalMapping?.provider?.ToString();
if (!string.IsNullOrEmpty(provider))
{
isLocal = false;
@@ -2137,6 +2148,29 @@ public class AdminController : ControllerBase
}
}
/// <summary>
/// Gets current active sessions for debugging.
/// </summary>
[HttpGet("sessions")]
public IActionResult GetActiveSessions()
{
try
{
var sessionManager = HttpContext.RequestServices.GetService<JellyfinSessionManager>();
if (sessionManager == null)
{
return BadRequest(new { error = "Session manager not available" });
}
var sessionInfo = sessionManager.GetSessionsInfo();
return Ok(sessionInfo);
}
catch (Exception ex)
{
return BadRequest(new { error = ex.Message });
}
}
/// <summary>
/// Helper method to trigger GC after large file operations to prevent memory leaks.
/// </summary>