mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
Websocket Proxying
This commit is contained in:
@@ -975,6 +975,24 @@ public class JellyfinController : ControllerBase
|
|||||||
|
|
||||||
var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(itemId);
|
var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(itemId);
|
||||||
|
|
||||||
|
// For local tracks, check if Jellyfin already has embedded lyrics
|
||||||
|
if (!isExternal)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Checking Jellyfin for embedded lyrics for local track: {ItemId}", itemId);
|
||||||
|
|
||||||
|
// Try to get lyrics from Jellyfin first (it reads embedded lyrics from files)
|
||||||
|
var (jellyfinLyrics, statusCode) = await _proxyService.GetJsonAsync($"Audio/{itemId}/Lyrics", null, Request.Headers);
|
||||||
|
|
||||||
|
if (jellyfinLyrics != null && statusCode == 200)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("✓ Found embedded lyrics in Jellyfin for track {ItemId}", itemId);
|
||||||
|
return new JsonResult(JsonSerializer.Deserialize<object>(jellyfinLyrics.RootElement.GetRawText()));
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("No embedded lyrics found in Jellyfin, falling back to LRCLIB search");
|
||||||
|
}
|
||||||
|
|
||||||
|
// For external tracks or when Jellyfin doesn't have lyrics, search LRCLIB
|
||||||
Song? song = null;
|
Song? song = null;
|
||||||
|
|
||||||
if (isExternal)
|
if (isExternal)
|
||||||
@@ -1004,6 +1022,7 @@ public class JellyfinController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to get lyrics from LRCLIB
|
// Try to get lyrics from LRCLIB
|
||||||
|
_logger.LogInformation("Searching LRCLIB for lyrics: {Artist} - {Title}", song.Artist, song.Title);
|
||||||
var lyricsService = HttpContext.RequestServices.GetService<LrclibService>();
|
var lyricsService = HttpContext.RequestServices.GetService<LrclibService>();
|
||||||
if (lyricsService == null)
|
if (lyricsService == null)
|
||||||
{
|
{
|
||||||
@@ -1668,7 +1687,9 @@ public class JellyfinController : ControllerBase
|
|||||||
}
|
}
|
||||||
Request.Body.Position = 0;
|
Request.Body.Position = 0;
|
||||||
|
|
||||||
_logger.LogInformation("📻 Playback START reported");
|
_logger.LogInformation("📻 Playback START reported - Body: {Body}", body);
|
||||||
|
_logger.LogInformation("Auth headers: {Headers}",
|
||||||
|
string.Join(", ", Request.Headers.Where(h => h.Key.Contains("Auth", StringComparison.OrdinalIgnoreCase)).Select(h => $"{h.Key}={h.Value}")));
|
||||||
|
|
||||||
// Parse the body to check if it's an external track
|
// Parse the body to check if it's an external track
|
||||||
var doc = JsonDocument.Parse(body);
|
var doc = JsonDocument.Parse(body);
|
||||||
@@ -1712,7 +1733,7 @@ public class JellyfinController : ControllerBase
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Playback start forward failed with status {StatusCode}", statusCode);
|
_logger.LogWarning("⚠️ Playback start forward returned status {StatusCode}", statusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
|
|||||||
209
allstarr/Middleware/WebSocketProxyMiddleware.cs
Normal file
209
allstarr/Middleware/WebSocketProxyMiddleware.cs
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
using System.Net.WebSockets;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using allstarr.Models.Settings;
|
||||||
|
|
||||||
|
namespace allstarr.Middleware;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Middleware that proxies WebSocket connections to Jellyfin server.
|
||||||
|
/// This enables real-time features like session tracking, remote control, and live updates.
|
||||||
|
/// </summary>
|
||||||
|
public class WebSocketProxyMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly JellyfinSettings _settings;
|
||||||
|
private readonly ILogger<WebSocketProxyMiddleware> _logger;
|
||||||
|
|
||||||
|
public WebSocketProxyMiddleware(
|
||||||
|
RequestDelegate next,
|
||||||
|
IOptions<JellyfinSettings> settings,
|
||||||
|
ILogger<WebSocketProxyMiddleware> logger)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_settings = settings.Value;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
// Check if this is a WebSocket request to /socket
|
||||||
|
if (context.Request.Path.StartsWithSegments("/socket", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
context.WebSockets.IsWebSocketRequest)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("🔌 WebSocket connection request received from {RemoteIp}",
|
||||||
|
context.Connection.RemoteIpAddress);
|
||||||
|
|
||||||
|
await HandleWebSocketProxyAsync(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not a WebSocket request, pass to next middleware
|
||||||
|
await _next(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleWebSocketProxyAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
ClientWebSocket? serverWebSocket = null;
|
||||||
|
WebSocket? clientWebSocket = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Accept the WebSocket connection from the client
|
||||||
|
clientWebSocket = await context.WebSockets.AcceptWebSocketAsync();
|
||||||
|
_logger.LogInformation("✓ Client WebSocket accepted");
|
||||||
|
|
||||||
|
// 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 query parameters if present (e.g., ?api_key=xxx or ?deviceId=xxx)
|
||||||
|
if (context.Request.QueryString.HasValue)
|
||||||
|
{
|
||||||
|
jellyfinWsUrl += context.Request.QueryString.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Connecting to Jellyfin WebSocket: {Url}", jellyfinWsUrl);
|
||||||
|
|
||||||
|
// Connect to Jellyfin WebSocket
|
||||||
|
serverWebSocket = new ClientWebSocket();
|
||||||
|
|
||||||
|
// Forward authentication headers
|
||||||
|
if (context.Request.Headers.TryGetValue("Authorization", out var authHeader))
|
||||||
|
{
|
||||||
|
serverWebSocket.Options.SetRequestHeader("Authorization", authHeader.ToString());
|
||||||
|
_logger.LogDebug("Forwarded Authorization header");
|
||||||
|
}
|
||||||
|
else if (context.Request.Headers.TryGetValue("X-Emby-Authorization", out var embyAuthHeader))
|
||||||
|
{
|
||||||
|
serverWebSocket.Options.SetRequestHeader("X-Emby-Authorization", embyAuthHeader.ToString());
|
||||||
|
_logger.LogDebug("Forwarded X-Emby-Authorization header");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set user agent
|
||||||
|
serverWebSocket.Options.SetRequestHeader("User-Agent", "Allstarr/1.0");
|
||||||
|
|
||||||
|
await serverWebSocket.ConnectAsync(new Uri(jellyfinWsUrl), context.RequestAborted);
|
||||||
|
_logger.LogInformation("✓ Connected to Jellyfin WebSocket");
|
||||||
|
|
||||||
|
// Start bidirectional proxying
|
||||||
|
var clientToServer = ProxyMessagesAsync(clientWebSocket, serverWebSocket, "Client→Server", context.RequestAborted);
|
||||||
|
var serverToClient = ProxyMessagesAsync(serverWebSocket, clientWebSocket, "Server→Client", context.RequestAborted);
|
||||||
|
|
||||||
|
// Wait for either direction to complete
|
||||||
|
await Task.WhenAny(clientToServer, serverToClient);
|
||||||
|
|
||||||
|
_logger.LogInformation("WebSocket proxy connection closed");
|
||||||
|
}
|
||||||
|
catch (WebSocketException wsEx)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(wsEx, "WebSocket error: {Message}", wsEx.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error in WebSocket proxy");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Clean up connections
|
||||||
|
if (clientWebSocket?.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Proxy closing", CancellationToken.None);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Error closing client WebSocket");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverWebSocket?.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await serverWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Proxy closing", CancellationToken.None);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Error closing server WebSocket");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clientWebSocket?.Dispose();
|
||||||
|
serverWebSocket?.Dispose();
|
||||||
|
|
||||||
|
_logger.LogInformation("WebSocket connections cleaned up");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProxyMessagesAsync(
|
||||||
|
WebSocket source,
|
||||||
|
WebSocket destination,
|
||||||
|
string direction,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var buffer = new byte[1024 * 4]; // 4KB buffer
|
||||||
|
var messageBuffer = new List<byte>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (source.State == WebSocketState.Open && destination.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
var result = await source.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken);
|
||||||
|
|
||||||
|
if (result.MessageType == WebSocketMessageType.Close)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("{Direction}: Close message received", direction);
|
||||||
|
await destination.CloseAsync(
|
||||||
|
result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
|
||||||
|
result.CloseStatusDescription,
|
||||||
|
cancellationToken);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accumulate message fragments
|
||||||
|
messageBuffer.AddRange(buffer.Take(result.Count));
|
||||||
|
|
||||||
|
// If this is the end of the message, forward it
|
||||||
|
if (result.EndOfMessage)
|
||||||
|
{
|
||||||
|
var messageBytes = messageBuffer.ToArray();
|
||||||
|
|
||||||
|
// Log message for debugging (only in debug mode to avoid spam)
|
||||||
|
if (_logger.IsEnabled(LogLevel.Debug))
|
||||||
|
{
|
||||||
|
var messageText = System.Text.Encoding.UTF8.GetString(messageBytes);
|
||||||
|
_logger.LogDebug("{Direction}: {MessageType} message ({Size} bytes): {Preview}",
|
||||||
|
direction,
|
||||||
|
result.MessageType,
|
||||||
|
messageBytes.Length,
|
||||||
|
messageText.Length > 200 ? messageText[..200] + "..." : messageText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward the complete message
|
||||||
|
await destination.SendAsync(
|
||||||
|
new ArraySegment<byte>(messageBytes),
|
||||||
|
result.MessageType,
|
||||||
|
true,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
messageBuffer.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("{Direction}: Operation cancelled", direction);
|
||||||
|
}
|
||||||
|
catch (WebSocketException wsEx) when (wsEx.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("{Direction}: Connection closed prematurely", direction);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "{Direction}: Error proxying messages", direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -288,6 +288,15 @@ app.UseExceptionHandler(_ => { }); // Global exception handler
|
|||||||
// Enable response compression EARLY in the pipeline
|
// Enable response compression EARLY in the pipeline
|
||||||
app.UseResponseCompression();
|
app.UseResponseCompression();
|
||||||
|
|
||||||
|
// Enable WebSocket support
|
||||||
|
app.UseWebSockets(new WebSocketOptions
|
||||||
|
{
|
||||||
|
KeepAliveInterval = TimeSpan.FromSeconds(120)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add WebSocket proxy middleware (BEFORE routing)
|
||||||
|
app.UseMiddleware<WebSocketProxyMiddleware>();
|
||||||
|
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseSwagger();
|
app.UseSwagger();
|
||||||
|
|||||||
Reference in New Issue
Block a user