mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
websocket on behalf of client
This commit is contained in:
@@ -1,31 +1,39 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using allstarr.Models.Settings;
|
||||||
|
|
||||||
namespace allstarr.Services.Jellyfin;
|
namespace allstarr.Services.Jellyfin;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manages Jellyfin sessions for connected clients.
|
/// Manages Jellyfin sessions for connected clients.
|
||||||
/// Creates sessions on first playback and keeps them alive with periodic pings.
|
/// Creates sessions on first playback and keeps them alive with periodic pings.
|
||||||
|
/// Also maintains server-side WebSocket connections to Jellyfin on behalf of clients.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class JellyfinSessionManager
|
public class JellyfinSessionManager : IDisposable
|
||||||
{
|
{
|
||||||
private readonly JellyfinProxyService _proxyService;
|
private readonly JellyfinProxyService _proxyService;
|
||||||
|
private readonly JellyfinSettings _settings;
|
||||||
private readonly ILogger<JellyfinSessionManager> _logger;
|
private readonly ILogger<JellyfinSessionManager> _logger;
|
||||||
private readonly ConcurrentDictionary<string, SessionInfo> _sessions = new();
|
private readonly ConcurrentDictionary<string, SessionInfo> _sessions = new();
|
||||||
private readonly Timer _keepAliveTimer;
|
private readonly Timer _keepAliveTimer;
|
||||||
|
|
||||||
public JellyfinSessionManager(
|
public JellyfinSessionManager(
|
||||||
JellyfinProxyService proxyService,
|
JellyfinProxyService proxyService,
|
||||||
|
IOptions<JellyfinSettings> settings,
|
||||||
ILogger<JellyfinSessionManager> logger)
|
ILogger<JellyfinSessionManager> logger)
|
||||||
{
|
{
|
||||||
_proxyService = proxyService;
|
_proxyService = proxyService;
|
||||||
|
_settings = settings.Value;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
// Keep sessions alive every 10 seconds (Jellyfin considers sessions stale after ~15 seconds of inactivity)
|
// Keep sessions alive every 10 seconds (Jellyfin considers sessions stale after ~15 seconds of inactivity)
|
||||||
_keepAliveTimer = new Timer(KeepSessionsAlive, null, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10));
|
_keepAliveTimer = new Timer(KeepSessionsAlive, null, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10));
|
||||||
|
|
||||||
_logger.LogWarning("🔧 SESSION: JellyfinSessionManager initialized with 10-second keep-alive");
|
_logger.LogWarning("🔧 SESSION: JellyfinSessionManager initialized with 10-second keep-alive and WebSocket support");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -70,6 +78,9 @@ public class JellyfinSessionManager
|
|||||||
Headers = CloneHeaders(headers)
|
Headers = CloneHeaders(headers)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Start a WebSocket connection to Jellyfin on behalf of this client
|
||||||
|
_ = Task.Run(() => MaintainWebSocketForSessionAsync(deviceId, headers));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -136,6 +147,24 @@ public class JellyfinSessionManager
|
|||||||
{
|
{
|
||||||
_logger.LogWarning("🗑️ SESSION: Removing session for device {DeviceId}", deviceId);
|
_logger.LogWarning("🗑️ SESSION: Removing session for device {DeviceId}", deviceId);
|
||||||
|
|
||||||
|
// Close WebSocket if it exists
|
||||||
|
if (session.WebSocket != null && session.WebSocket.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await session.WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Session ended", CancellationToken.None);
|
||||||
|
_logger.LogWarning("🔌 WEBSOCKET: Closed WebSocket for device {DeviceId}", deviceId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "⚠️ WEBSOCKET: Error closing WebSocket for {DeviceId}", deviceId);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
session.WebSocket?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Optionally notify Jellyfin that the session is ending
|
// Optionally notify Jellyfin that the session is ending
|
||||||
@@ -149,6 +178,124 @@ public class JellyfinSessionManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maintains a WebSocket connection to Jellyfin on behalf of a client session.
|
||||||
|
/// This allows the session to appear in Jellyfin's dashboard.
|
||||||
|
/// </summary>
|
||||||
|
private async Task MaintainWebSocketForSessionAsync(string deviceId, IHeaderDictionary headers)
|
||||||
|
{
|
||||||
|
if (!_sessions.TryGetValue(deviceId, out var session))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("⚠️ WEBSOCKET: Cannot create WebSocket - session {DeviceId} not found", deviceId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientWebSocket? webSocket = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Build Jellyfin WebSocket URL
|
||||||
|
var jellyfinUrl = _settings.Url?.TrimEnd('/') ?? "";
|
||||||
|
var wsScheme = jellyfinUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ? "wss://" : "ws://";
|
||||||
|
var jellyfinHost = jellyfinUrl.Replace("https://", "").Replace("http://", "");
|
||||||
|
var jellyfinWsUrl = $"{wsScheme}{jellyfinHost}/socket";
|
||||||
|
|
||||||
|
// Add API key if available
|
||||||
|
if (!string.IsNullOrEmpty(_settings.ApiKey))
|
||||||
|
{
|
||||||
|
jellyfinWsUrl += $"?api_key={_settings.ApiKey}";
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogWarning("🔗 WEBSOCKET: Connecting to Jellyfin for device {DeviceId}: {Url}", deviceId, jellyfinWsUrl);
|
||||||
|
|
||||||
|
webSocket = new ClientWebSocket();
|
||||||
|
session.WebSocket = webSocket;
|
||||||
|
|
||||||
|
// Forward authentication headers
|
||||||
|
if (headers.TryGetValue("X-Emby-Authorization", out var embyAuth))
|
||||||
|
{
|
||||||
|
webSocket.Options.SetRequestHeader("X-Emby-Authorization", embyAuth.ToString());
|
||||||
|
_logger.LogWarning("🔑 WEBSOCKET: Forwarded X-Emby-Authorization for {DeviceId}", deviceId);
|
||||||
|
}
|
||||||
|
else if (headers.TryGetValue("Authorization", out var auth))
|
||||||
|
{
|
||||||
|
var authValue = auth.ToString();
|
||||||
|
if (authValue.Contains("MediaBrowser", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
webSocket.Options.SetRequestHeader("X-Emby-Authorization", authValue);
|
||||||
|
_logger.LogWarning("🔑 WEBSOCKET: Converted Authorization to X-Emby-Authorization for {DeviceId}", deviceId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
webSocket.Options.SetRequestHeader("Authorization", authValue);
|
||||||
|
_logger.LogWarning("🔑 WEBSOCKET: Forwarded Authorization for {DeviceId}", deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set user agent
|
||||||
|
webSocket.Options.SetRequestHeader("User-Agent", $"Allstarr-Proxy/{session.Client}");
|
||||||
|
|
||||||
|
// Connect to Jellyfin
|
||||||
|
await webSocket.ConnectAsync(new Uri(jellyfinWsUrl), CancellationToken.None);
|
||||||
|
_logger.LogWarning("✓ WEBSOCKET: Connected to Jellyfin for device {DeviceId}", deviceId);
|
||||||
|
|
||||||
|
// Keep the WebSocket alive by reading messages
|
||||||
|
var buffer = new byte[1024 * 4];
|
||||||
|
while (webSocket.State == WebSocketState.Open && _sessions.ContainsKey(deviceId))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||||
|
|
||||||
|
if (result.MessageType == WebSocketMessageType.Close)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("🔌 WEBSOCKET: Jellyfin closed WebSocket for device {DeviceId}", deviceId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log received messages for debugging
|
||||||
|
if (result.MessageType == WebSocketMessageType.Text)
|
||||||
|
{
|
||||||
|
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
|
||||||
|
_logger.LogWarning("📥 WEBSOCKET: Received from Jellyfin for {DeviceId}: {Message}",
|
||||||
|
deviceId, message.Length > 100 ? message[..100] + "..." : message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (WebSocketException wsEx)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(wsEx, "⚠️ WEBSOCKET: WebSocket error for device {DeviceId}", deviceId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "❌ WEBSOCKET: Failed to maintain WebSocket for device {DeviceId}", deviceId);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (webSocket != null)
|
||||||
|
{
|
||||||
|
if (webSocket.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Session ended", CancellationToken.None);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
webSocket.Dispose();
|
||||||
|
_logger.LogWarning("🧹 WEBSOCKET: Cleaned up WebSocket for device {DeviceId}", deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear WebSocket reference from session
|
||||||
|
if (_sessions.TryGetValue(deviceId, out var sess))
|
||||||
|
{
|
||||||
|
sess.WebSocket = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Periodically pings Jellyfin to keep sessions alive.
|
/// Periodically pings Jellyfin to keep sessions alive.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -205,5 +352,28 @@ public class JellyfinSessionManager
|
|||||||
public required string Version { get; init; }
|
public required string Version { get; init; }
|
||||||
public DateTime LastActivity { get; set; }
|
public DateTime LastActivity { get; set; }
|
||||||
public required IHeaderDictionary Headers { get; init; }
|
public required IHeaderDictionary Headers { get; init; }
|
||||||
|
public ClientWebSocket? WebSocket { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_keepAliveTimer?.Dispose();
|
||||||
|
|
||||||
|
// Close all WebSocket connections
|
||||||
|
foreach (var session in _sessions.Values)
|
||||||
|
{
|
||||||
|
if (session.WebSocket != null && session.WebSocket.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
session.WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Service stopping", CancellationToken.None).Wait(TimeSpan.FromSeconds(5));
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
session.WebSocket?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user