Merge branch 'master' into activitydb-efcore

This commit is contained in:
Patrick Barron 2020-05-14 21:16:43 +00:00 committed by GitHub
commit 87c63e668c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 460 additions and 1235 deletions

View File

@ -908,7 +908,8 @@ namespace Emby.Dlna.PlayTo
return 0; return 0;
} }
public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken) /// <inheritdoc />
public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken)
{ {
if (_disposed) if (_disposed)
{ {
@ -924,10 +925,12 @@ namespace Emby.Dlna.PlayTo
{ {
return SendPlayCommand(data as PlayRequest, cancellationToken); return SendPlayCommand(data as PlayRequest, cancellationToken);
} }
if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase)) if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
{ {
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken); return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
} }
if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase)) if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
{ {
return SendGeneralCommand(data as GeneralCommand, cancellationToken); return SendGeneralCommand(data as GeneralCommand, cancellationToken);

View File

@ -43,7 +43,6 @@ using Emby.Server.Implementations.Security;
using Emby.Server.Implementations.Serialization; using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Services; using Emby.Server.Implementations.Services;
using Emby.Server.Implementations.Session; using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SocketSharp;
using Emby.Server.Implementations.TV; using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates; using Emby.Server.Implementations.Updates;
using MediaBrowser.Api; using MediaBrowser.Api;
@ -100,12 +99,11 @@ using MediaBrowser.Providers.Subtitles;
using MediaBrowser.WebDashboard.Api; using MediaBrowser.WebDashboard.Api;
using MediaBrowser.XbmcMetadata.Providers; using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
using Prometheus.DotNetRuntime; using Prometheus.DotNetRuntime;
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
namespace Emby.Server.Implementations namespace Emby.Server.Implementations
{ {
@ -502,32 +500,8 @@ namespace Emby.Server.Implementations
RegisterServices(serviceCollection); RegisterServices(serviceCollection);
} }
public async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next) public Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
{ => _httpServer.RequestHandler(context);
if (!context.WebSockets.IsWebSocketRequest)
{
await next().ConfigureAwait(false);
return;
}
await _httpServer.ProcessWebSocketRequest(context).ConfigureAwait(false);
}
public async Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
{
if (context.WebSockets.IsWebSocketRequest)
{
await next().ConfigureAwait(false);
return;
}
var request = context.Request;
var response = context.Response;
var localPath = context.Request.Path.ToString();
var req = new WebSocketSharpRequest(request, response, request.Path, LoggerFactory.CreateLogger<WebSocketSharpRequest>());
await _httpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted).ConfigureAwait(false);
}
/// <summary> /// <summary>
/// Registers services/resources with the service collection that will be available via DI. /// Registers services/resources with the service collection that will be available via DI.
@ -613,7 +587,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<ISearchEngine, SearchEngine>(); serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
serviceCollection.AddSingleton<ServiceController>(); serviceCollection.AddSingleton<ServiceController>();
serviceCollection.AddSingleton<IHttpListener, WebSocketSharpListener>();
serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>(); serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>(); serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>();

View File

@ -6,11 +6,12 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Sockets; using System.Net.Sockets;
using System.Net.WebSockets;
using System.Reflection; using System.Reflection;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Server.Implementations.Net;
using Emby.Server.Implementations.Services; using Emby.Server.Implementations.Services;
using Emby.Server.Implementations.SocketSharp;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller; using MediaBrowser.Controller;
@ -22,6 +23,7 @@ using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.WebUtilities; using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
@ -30,7 +32,7 @@ using ServiceStack.Text.Jsv;
namespace Emby.Server.Implementations.HttpServer namespace Emby.Server.Implementations.HttpServer
{ {
public class HttpListenerHost : IHttpServer, IDisposable public class HttpListenerHost : IHttpServer
{ {
/// <summary> /// <summary>
/// The key for a setting that specifies the default redirect path /// The key for a setting that specifies the default redirect path
@ -39,17 +41,17 @@ namespace Emby.Server.Implementations.HttpServer
public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath"; public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath";
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ILoggerFactory _loggerFactory;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly INetworkManager _networkManager; private readonly INetworkManager _networkManager;
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private readonly IXmlSerializer _xmlSerializer; private readonly IXmlSerializer _xmlSerializer;
private readonly IHttpListener _socketListener;
private readonly Func<Type, Func<string, object>> _funcParseFn; private readonly Func<Type, Func<string, object>> _funcParseFn;
private readonly string _defaultRedirectPath; private readonly string _defaultRedirectPath;
private readonly string _baseUrlPrefix; private readonly string _baseUrlPrefix;
private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>(); private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>();
private readonly List<IWebSocketConnection> _webSocketConnections = new List<IWebSocketConnection>();
private readonly IHostEnvironment _hostEnvironment; private readonly IHostEnvironment _hostEnvironment;
private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>(); private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
@ -63,10 +65,10 @@ namespace Emby.Server.Implementations.HttpServer
INetworkManager networkManager, INetworkManager networkManager,
IJsonSerializer jsonSerializer, IJsonSerializer jsonSerializer,
IXmlSerializer xmlSerializer, IXmlSerializer xmlSerializer,
IHttpListener socketListener,
ILocalizationManager localizationManager, ILocalizationManager localizationManager,
ServiceController serviceController, ServiceController serviceController,
IHostEnvironment hostEnvironment) IHostEnvironment hostEnvironment,
ILoggerFactory loggerFactory)
{ {
_appHost = applicationHost; _appHost = applicationHost;
_logger = logger; _logger = logger;
@ -76,11 +78,9 @@ namespace Emby.Server.Implementations.HttpServer
_networkManager = networkManager; _networkManager = networkManager;
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
_xmlSerializer = xmlSerializer; _xmlSerializer = xmlSerializer;
_socketListener = socketListener;
ServiceController = serviceController; ServiceController = serviceController;
_socketListener.WebSocketConnected = OnWebSocketConnected;
_hostEnvironment = hostEnvironment; _hostEnvironment = hostEnvironment;
_loggerFactory = loggerFactory;
_funcParseFn = t => s => JsvReader.GetParseFn(t)(s); _funcParseFn = t => s => JsvReader.GetParseFn(t)(s);
@ -172,38 +172,6 @@ namespace Emby.Server.Implementations.HttpServer
return attributes; return attributes;
} }
private void OnWebSocketConnected(WebSocketConnectEventArgs e)
{
if (_disposed)
{
return;
}
var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger)
{
OnReceive = ProcessWebSocketMessageReceived,
Url = e.Url,
QueryString = e.QueryString
};
connection.Closed += OnConnectionClosed;
lock (_webSocketConnections)
{
_webSocketConnections.Add(connection);
}
WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
}
private void OnConnectionClosed(object sender, EventArgs e)
{
lock (_webSocketConnections)
{
_webSocketConnections.Remove((IWebSocketConnection)sender);
}
}
private static Exception GetActualException(Exception ex) private static Exception GetActualException(Exception ex)
{ {
if (ex is AggregateException agg) if (ex is AggregateException agg)
@ -289,32 +257,6 @@ namespace Emby.Server.Implementations.HttpServer
.Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase); .Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase);
} }
/// <summary>
/// Shut down the Web Service
/// </summary>
public void Stop()
{
List<IWebSocketConnection> connections;
lock (_webSocketConnections)
{
connections = _webSocketConnections.ToList();
_webSocketConnections.Clear();
}
foreach (var connection in connections)
{
try
{
connection.Dispose();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error disposing connection");
}
}
}
public static string RemoveQueryStringByKey(string url, string key) public static string RemoveQueryStringByKey(string url, string key)
{ {
var uri = new Uri(url); var uri = new Uri(url);
@ -430,9 +372,9 @@ namespace Emby.Server.Implementations.HttpServer
/// <returns>True if the request is valid, or false if the request is not valid and an HTTPS redirect is required.</returns> /// <returns>True if the request is valid, or false if the request is not valid and an HTTPS redirect is required.</returns>
private bool ValidateSsl(string remoteIp, string urlString) private bool ValidateSsl(string remoteIp, string urlString)
{ {
if (_config.Configuration.RequireHttps && _appHost.ListenWithHttps) if (_config.Configuration.RequireHttps
{ && _appHost.ListenWithHttps
if (urlString.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == -1) && !urlString.Contains("https://", StringComparison.OrdinalIgnoreCase))
{ {
// These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1 if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
@ -446,15 +388,30 @@ namespace Emby.Server.Implementations.HttpServer
return false; return false;
} }
} }
}
return true; return true;
} }
/// <inheritdoc />
public Task RequestHandler(HttpContext context)
{
if (context.WebSockets.IsWebSocketRequest)
{
return WebSocketRequestHandler(context);
}
var request = context.Request;
var response = context.Response;
var localPath = context.Request.Path.ToString();
var req = new WebSocketSharpRequest(request, response, request.Path, _logger);
return RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted);
}
/// <summary> /// <summary>
/// Overridable method that can be used to implement a custom handler. /// Overridable method that can be used to implement a custom handler.
/// </summary> /// </summary>
public async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken) private async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
{ {
var stopWatch = new Stopwatch(); var stopWatch = new Stopwatch();
stopWatch.Start(); stopWatch.Start();
@ -582,6 +539,43 @@ namespace Emby.Server.Implementations.HttpServer
} }
} }
private async Task WebSocketRequestHandler(HttpContext context)
{
if (_disposed)
{
return;
}
try
{
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
var connection = new WebSocketConnection(
_loggerFactory.CreateLogger<WebSocketConnection>(),
webSocket,
context.Connection.RemoteIpAddress,
context.Request.Query)
{
OnReceive = ProcessWebSocketMessageReceived
};
WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
await connection.ProcessAsync().ConfigureAwait(false);
_logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
}
catch (Exception ex) // Otherwise ASP.Net will ignore the exception
{
_logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress);
if (!context.Response.HasStarted)
{
context.Response.StatusCode = 500;
}
}
}
// Entry point for HttpListener // Entry point for HttpListener
public ServiceHandler GetServiceHandler(IHttpRequest httpReq) public ServiceHandler GetServiceHandler(IHttpRequest httpReq)
{ {
@ -689,11 +683,6 @@ namespace Emby.Server.Implementations.HttpServer
return _jsonSerializer.DeserializeFromStreamAsync(stream, type); return _jsonSerializer.DeserializeFromStreamAsync(stream, type);
} }
public Task ProcessWebSocketRequest(HttpContext context)
{
return _socketListener.ProcessWebSocketRequest(context);
}
private string NormalizeEmbyRoutePath(string path) private string NormalizeEmbyRoutePath(string path)
{ {
_logger.LogDebug("Normalizing /emby route"); _logger.LogDebug("Normalizing /emby route");
@ -712,28 +701,6 @@ namespace Emby.Server.Implementations.HttpServer
return _baseUrlPrefix + NormalizeUrlPath(path); return _baseUrlPrefix + NormalizeUrlPath(path);
} }
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
Stop();
}
_disposed = true;
}
/// <summary> /// <summary>
/// Processes the web socket message received. /// Processes the web socket message received.
/// </summary> /// </summary>
@ -745,8 +712,6 @@ namespace Emby.Server.Implementations.HttpServer
return Task.CompletedTask; return Task.CompletedTask;
} }
_logger.LogDebug("Websocket message received: {0}", result.MessageType);
IEnumerable<Task> GetTasks() IEnumerable<Task> GetTasks()
{ {
foreach (var x in _webSocketListeners) foreach (var x in _webSocketListeners)

View File

@ -1,39 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Net;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace Emby.Server.Implementations.HttpServer
{
public interface IHttpListener : IDisposable
{
/// <summary>
/// Gets or sets the error handler.
/// </summary>
/// <value>The error handler.</value>
Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; }
/// <summary>
/// Gets or sets the request handler.
/// </summary>
/// <value>The request handler.</value>
Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; }
/// <summary>
/// Gets or sets the web socket handler.
/// </summary>
/// <value>The web socket handler.</value>
Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
/// <summary>
/// Stops this instance.
/// </summary>
Task Stop();
Task ProcessWebSocketRequest(HttpContext ctx);
}
}

View File

@ -1,15 +1,18 @@
using System; #nullable enable
using System;
using System.Buffers;
using System.IO.Pipelines;
using System.Net;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Text; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Server.Implementations.Net; using MediaBrowser.Common.Json;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using UtfUnknown;
namespace Emby.Server.Implementations.HttpServer namespace Emby.Server.Implementations.HttpServer
{ {
@ -24,69 +27,50 @@ namespace Emby.Server.Implementations.HttpServer
private readonly ILogger _logger; private readonly ILogger _logger;
/// <summary> /// <summary>
/// The json serializer. /// The json serializer options.
/// </summary> /// </summary>
private readonly IJsonSerializer _jsonSerializer; private readonly JsonSerializerOptions _jsonOptions;
/// <summary> /// <summary>
/// The socket. /// The socket.
/// </summary> /// </summary>
private readonly IWebSocket _socket; private readonly WebSocket _socket;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WebSocketConnection" /> class. /// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
/// </summary> /// </summary>
/// <param name="logger">The logger.</param>
/// <param name="socket">The socket.</param> /// <param name="socket">The socket.</param>
/// <param name="remoteEndPoint">The remote end point.</param> /// <param name="remoteEndPoint">The remote end point.</param>
/// <param name="jsonSerializer">The json serializer.</param> /// <param name="query">The query.</param>
/// <param name="logger">The logger.</param> public WebSocketConnection(
/// <exception cref="ArgumentNullException">socket</exception> ILogger<WebSocketConnection> logger,
public WebSocketConnection(IWebSocket socket, string remoteEndPoint, IJsonSerializer jsonSerializer, ILogger logger) WebSocket socket,
IPAddress? remoteEndPoint,
IQueryCollection query)
{ {
if (socket == null)
{
throw new ArgumentNullException(nameof(socket));
}
if (string.IsNullOrEmpty(remoteEndPoint))
{
throw new ArgumentNullException(nameof(remoteEndPoint));
}
if (jsonSerializer == null)
{
throw new ArgumentNullException(nameof(jsonSerializer));
}
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
Id = Guid.NewGuid();
_jsonSerializer = jsonSerializer;
_socket = socket;
_socket.OnReceiveBytes = OnReceiveInternal;
RemoteEndPoint = remoteEndPoint;
_logger = logger; _logger = logger;
_socket = socket;
RemoteEndPoint = remoteEndPoint;
QueryString = query;
socket.Closed += OnSocketClosed; _jsonOptions = JsonDefaults.GetOptions();
LastActivityDate = DateTime.Now;
} }
/// <inheritdoc /> /// <inheritdoc />
public event EventHandler<EventArgs> Closed; public event EventHandler<EventArgs>? Closed;
/// <summary> /// <summary>
/// Gets or sets the remote end point. /// Gets or sets the remote end point.
/// </summary> /// </summary>
public string RemoteEndPoint { get; private set; } public IPAddress? RemoteEndPoint { get; }
/// <summary> /// <summary>
/// Gets or sets the receive action. /// Gets or sets the receive action.
/// </summary> /// </summary>
/// <value>The receive action.</value> /// <value>The receive action.</value>
public Func<WebSocketMessageInfo, Task> OnReceive { get; set; } public Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
/// <summary> /// <summary>
/// Gets the last activity date. /// Gets the last activity date.
@ -94,23 +78,11 @@ namespace Emby.Server.Implementations.HttpServer
/// <value>The last activity date.</value> /// <value>The last activity date.</value>
public DateTime LastActivityDate { get; private set; } public DateTime LastActivityDate { get; private set; }
/// <summary>
/// Gets the id.
/// </summary>
/// <value>The id.</value>
public Guid Id { get; private set; }
/// <summary>
/// Gets or sets the URL.
/// </summary>
/// <value>The URL.</value>
public string Url { get; set; }
/// <summary> /// <summary>
/// Gets or sets the query string. /// Gets or sets the query string.
/// </summary> /// </summary>
/// <value>The query string.</value> /// <value>The query string.</value>
public IQueryCollection QueryString { get; set; } public IQueryCollection QueryString { get; }
/// <summary> /// <summary>
/// Gets the state. /// Gets the state.
@ -118,70 +90,6 @@ namespace Emby.Server.Implementations.HttpServer
/// <value>The state.</value> /// <value>The state.</value>
public WebSocketState State => _socket.State; public WebSocketState State => _socket.State;
void OnSocketClosed(object sender, EventArgs e)
{
Closed?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Called when [receive].
/// </summary>
/// <param name="bytes">The bytes.</param>
private void OnReceiveInternal(byte[] bytes)
{
LastActivityDate = DateTime.UtcNow;
if (OnReceive == null)
{
return;
}
var charset = CharsetDetector.DetectFromBytes(bytes).Detected?.EncodingName;
if (string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase))
{
OnReceiveInternal(Encoding.UTF8.GetString(bytes, 0, bytes.Length));
}
else
{
OnReceiveInternal(Encoding.ASCII.GetString(bytes, 0, bytes.Length));
}
}
private void OnReceiveInternal(string message)
{
LastActivityDate = DateTime.UtcNow;
if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase))
{
// This info is useful sometimes but also clogs up the log
_logger.LogDebug("Received web socket message that is not a json structure: {message}", message);
return;
}
if (OnReceive == null)
{
return;
}
try
{
var stub = (WebSocketMessage<object>)_jsonSerializer.DeserializeFromString(message, typeof(WebSocketMessage<object>));
var info = new WebSocketMessageInfo
{
MessageType = stub.MessageType,
Data = stub.Data?.ToString(),
Connection = this
};
OnReceive(info);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing web socket message");
}
}
/// <summary> /// <summary>
/// Sends a message asynchronously. /// Sends a message asynchronously.
/// </summary> /// </summary>
@ -189,67 +97,128 @@ namespace Emby.Server.Implementations.HttpServer
/// <param name="message">The message.</param> /// <param name="message">The message.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns> /// <returns>Task.</returns>
/// <exception cref="ArgumentNullException">message</exception>
public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken) public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
{ {
if (message == null) var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
{ return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
throw new ArgumentNullException(nameof(message));
}
var json = _jsonSerializer.SerializeToString(message);
return SendAsync(json, cancellationToken);
}
/// <summary>
/// Sends a message asynchronously.
/// </summary>
/// <param name="buffer">The buffer.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public Task SendAsync(byte[] buffer, CancellationToken cancellationToken)
{
if (buffer == null)
{
throw new ArgumentNullException(nameof(buffer));
}
cancellationToken.ThrowIfCancellationRequested();
return _socket.SendAsync(buffer, true, cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
public Task SendAsync(string text, CancellationToken cancellationToken) public async Task ProcessAsync(CancellationToken cancellationToken = default)
{ {
if (string.IsNullOrEmpty(text)) var pipe = new Pipe();
var writer = pipe.Writer;
ValueWebSocketReceiveResult receiveresult;
do
{ {
throw new ArgumentNullException(nameof(text)); // Allocate at least 512 bytes from the PipeWriter
Memory<byte> memory = writer.GetMemory(512);
try
{
receiveresult = await _socket.ReceiveAsync(memory, cancellationToken);
}
catch (WebSocketException ex)
{
_logger.LogWarning("WS {IP} error receiving data: {Message}", RemoteEndPoint, ex.Message);
break;
} }
cancellationToken.ThrowIfCancellationRequested(); int bytesRead = receiveresult.Count;
if (bytesRead == 0)
return _socket.SendAsync(text, true, cancellationToken); {
break;
} }
/// <inheritdoc /> // Tell the PipeWriter how much was read from the Socket
public void Dispose() writer.Advance(bytesRead);
// Make the data available to the PipeReader
FlushResult flushResult = await writer.FlushAsync();
if (flushResult.IsCompleted)
{ {
Dispose(true); // The PipeReader stopped reading
GC.SuppressFinalize(this); break;
} }
/// <summary> LastActivityDate = DateTime.UtcNow;
/// Releases unmanaged and - optionally - managed resources.
/// </summary> if (receiveresult.EndOfMessage)
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool dispose)
{ {
if (dispose) await ProcessInternal(pipe.Reader).ConfigureAwait(false);
{
_socket.Dispose();
} }
} while (
(_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
&& receiveresult.MessageType != WebSocketMessageType.Close);
Closed?.Invoke(this, EventArgs.Empty);
if (_socket.State == WebSocketState.Open
|| _socket.State == WebSocketState.CloseReceived
|| _socket.State == WebSocketState.CloseSent)
{
await _socket.CloseAsync(
WebSocketCloseStatus.NormalClosure,
string.Empty,
cancellationToken).ConfigureAwait(false);
}
}
private async Task ProcessInternal(PipeReader reader)
{
ReadResult result = await reader.ReadAsync().ConfigureAwait(false);
ReadOnlySequence<byte> buffer = result.Buffer;
if (OnReceive == null)
{
// Tell the PipeReader how much of the buffer we have consumed
reader.AdvanceTo(buffer.End);
return;
}
WebSocketMessage<object> stub;
try
{
if (buffer.IsSingleSegment)
{
stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buffer.FirstSpan, _jsonOptions);
}
else
{
var buf = ArrayPool<byte>.Shared.Rent(Convert.ToInt32(buffer.Length));
try
{
buffer.CopyTo(buf);
stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buf, _jsonOptions);
}
finally
{
ArrayPool<byte>.Shared.Return(buf);
}
}
}
catch (JsonException ex)
{
// Tell the PipeReader how much of the buffer we have consumed
reader.AdvanceTo(buffer.End);
_logger.LogError(ex, "Error processing web socket message");
return;
}
// Tell the PipeReader how much of the buffer we have consumed
reader.AdvanceTo(buffer.End);
_logger.LogDebug("WS {IP} received message: {@Message}", RemoteEndPoint, stub);
var info = new WebSocketMessageInfo
{
MessageType = stub.MessageType,
Data = stub.Data?.ToString(), // Data can be null
Connection = this
};
await OnReceive(info).ConfigureAwait(false);
} }
} }
} }

View File

@ -1,39 +0,0 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using WebSocketManager = Emby.Server.Implementations.WebSockets.WebSocketManager;
namespace Emby.Server.Implementations.Middleware
{
public class WebSocketMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<WebSocketMiddleware> _logger;
private readonly WebSocketManager _webSocketManager;
public WebSocketMiddleware(RequestDelegate next, ILogger<WebSocketMiddleware> logger, WebSocketManager webSocketManager)
{
_next = next;
_logger = logger;
_webSocketManager = webSocketManager;
}
public async Task Invoke(HttpContext httpContext)
{
_logger.LogInformation("Handling request: " + httpContext.Request.Path);
if (httpContext.WebSockets.IsWebSocketRequest)
{
var webSocketContext = await httpContext.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false);
if (webSocketContext != null)
{
await _webSocketManager.OnWebSocketConnected(webSocketContext).ConfigureAwait(false);
}
}
else
{
await _next.Invoke(httpContext).ConfigureAwait(false);
}
}
}
}

View File

@ -1,48 +0,0 @@
using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
namespace Emby.Server.Implementations.Net
{
/// <summary>
/// Interface IWebSocket
/// </summary>
public interface IWebSocket : IDisposable
{
/// <summary>
/// Occurs when [closed].
/// </summary>
event EventHandler<EventArgs> Closed;
/// <summary>
/// Gets or sets the state.
/// </summary>
/// <value>The state.</value>
WebSocketState State { get; }
/// <summary>
/// Gets or sets the receive action.
/// </summary>
/// <value>The receive action.</value>
Action<byte[]> OnReceiveBytes { get; set; }
/// <summary>
/// Sends the async.
/// </summary>
/// <param name="bytes">The bytes.</param>
/// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken);
/// <summary>
/// Sends the asynchronous.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken);
}
}

View File

@ -1,29 +0,0 @@
using System;
using Microsoft.AspNetCore.Http;
namespace Emby.Server.Implementations.Net
{
public class WebSocketConnectEventArgs : EventArgs
{
/// <summary>
/// Gets or sets the URL.
/// </summary>
/// <value>The URL.</value>
public string Url { get; set; }
/// <summary>
/// Gets or sets the query string.
/// </summary>
/// <value>The query string.</value>
public IQueryCollection QueryString { get; set; }
/// <summary>
/// Gets or sets the web socket.
/// </summary>
/// <value>The web socket.</value>
public IWebSocket WebSocket { get; set; }
/// <summary>
/// Gets or sets the endpoint.
/// </summary>
/// <value>The endpoint.</value>
public string Endpoint { get; set; }
}
}

View File

@ -1,191 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Session;
namespace Emby.Server.Implementations.Session
{
public class HttpSessionController : ISessionController
{
private readonly IHttpClient _httpClient;
private readonly IJsonSerializer _json;
private readonly ISessionManager _sessionManager;
public SessionInfo Session { get; private set; }
private readonly string _postUrl;
public HttpSessionController(IHttpClient httpClient,
IJsonSerializer json,
SessionInfo session,
string postUrl, ISessionManager sessionManager)
{
_httpClient = httpClient;
_json = json;
Session = session;
_postUrl = postUrl;
_sessionManager = sessionManager;
}
private string PostUrl => string.Format("http://{0}{1}", Session.RemoteEndPoint, _postUrl);
public bool IsSessionActive => (DateTime.UtcNow - Session.LastActivityDate).TotalMinutes <= 5;
public bool SupportsMediaControl => true;
private Task SendMessage(string name, string messageId, CancellationToken cancellationToken)
{
return SendMessage(name, messageId, new Dictionary<string, string>(), cancellationToken);
}
private Task SendMessage(string name, string messageId, Dictionary<string, string> args, CancellationToken cancellationToken)
{
args["messageId"] = messageId;
var url = PostUrl + "/" + name + ToQueryString(args);
return SendRequest(new HttpRequestOptions
{
Url = url,
CancellationToken = cancellationToken,
BufferContent = false
});
}
private Task SendPlayCommand(PlayRequest command, string messageId, CancellationToken cancellationToken)
{
var dict = new Dictionary<string, string>();
dict["ItemIds"] = string.Join(",", command.ItemIds.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
if (command.StartPositionTicks.HasValue)
{
dict["StartPositionTicks"] = command.StartPositionTicks.Value.ToString(CultureInfo.InvariantCulture);
}
if (command.AudioStreamIndex.HasValue)
{
dict["AudioStreamIndex"] = command.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture);
}
if (command.SubtitleStreamIndex.HasValue)
{
dict["SubtitleStreamIndex"] = command.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture);
}
if (command.StartIndex.HasValue)
{
dict["StartIndex"] = command.StartIndex.Value.ToString(CultureInfo.InvariantCulture);
}
if (!string.IsNullOrEmpty(command.MediaSourceId))
{
dict["MediaSourceId"] = command.MediaSourceId;
}
return SendMessage(command.PlayCommand.ToString(), messageId, dict, cancellationToken);
}
private Task SendPlaystateCommand(PlaystateRequest command, string messageId, CancellationToken cancellationToken)
{
var args = new Dictionary<string, string>();
if (command.Command == PlaystateCommand.Seek)
{
if (!command.SeekPositionTicks.HasValue)
{
throw new ArgumentException("SeekPositionTicks cannot be null");
}
args["SeekPositionTicks"] = command.SeekPositionTicks.Value.ToString(CultureInfo.InvariantCulture);
}
return SendMessage(command.Command.ToString(), messageId, args, cancellationToken);
}
private string[] _supportedMessages = Array.Empty<string>();
public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken)
{
if (!IsSessionActive)
{
return Task.CompletedTask;
}
if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
{
return SendPlayCommand(data as PlayRequest, messageId, cancellationToken);
}
if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
{
return SendPlaystateCommand(data as PlaystateRequest, messageId, cancellationToken);
}
if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
{
var command = data as GeneralCommand;
return SendMessage(command.Name, messageId, command.Arguments, cancellationToken);
}
if (!_supportedMessages.Contains(name, StringComparer.OrdinalIgnoreCase))
{
return Task.CompletedTask;
}
var url = PostUrl + "/" + name;
url += "?messageId=" + messageId;
var options = new HttpRequestOptions
{
Url = url,
CancellationToken = cancellationToken,
BufferContent = false
};
if (data != null)
{
if (typeof(T) == typeof(string))
{
var str = data as string;
if (!string.IsNullOrEmpty(str))
{
options.RequestContent = str;
options.RequestContentType = "application/json";
}
}
else
{
options.RequestContent = _json.SerializeToString(data);
options.RequestContentType = "application/json";
}
}
return SendRequest(options);
}
private async Task SendRequest(HttpRequestOptions options)
{
using (var response = await _httpClient.Post(options).ConfigureAwait(false))
{
}
}
private static string ToQueryString(Dictionary<string, string> nvc)
{
var array = (from item in nvc
select string.Format("{0}={1}", WebUtility.UrlEncode(item.Key), WebUtility.UrlEncode(item.Value)))
.ToArray();
var args = string.Join("&", array);
if (string.IsNullOrEmpty(args))
{
return args;
}
return "?" + args;
}
}
}

View File

@ -477,8 +477,7 @@ namespace Emby.Server.Implementations.Session
Client = appName, Client = appName,
DeviceId = deviceId, DeviceId = deviceId,
ApplicationVersion = appVersion, ApplicationVersion = appVersion,
Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture), Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture)
ServerId = _appHost.SystemId
}; };
var username = user?.Name; var username = user?.Name;
@ -1042,12 +1041,12 @@ namespace Emby.Server.Implementations.Session
private static async Task SendMessageToSession<T>(SessionInfo session, string name, T data, CancellationToken cancellationToken) private static async Task SendMessageToSession<T>(SessionInfo session, string name, T data, CancellationToken cancellationToken)
{ {
var controllers = session.SessionControllers.ToArray(); var controllers = session.SessionControllers;
var messageId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); var messageId = Guid.NewGuid();
foreach (var controller in controllers) foreach (var controller in controllers)
{ {
await controller.SendMessage(name, messageId, data, controllers, cancellationToken).ConfigureAwait(false); await controller.SendMessage(name, messageId, data, cancellationToken).ConfigureAwait(false);
} }
} }
@ -1055,13 +1054,13 @@ namespace Emby.Server.Implementations.Session
{ {
IEnumerable<Task> GetTasks() IEnumerable<Task> GetTasks()
{ {
var messageId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); var messageId = Guid.NewGuid();
foreach (var session in sessions) foreach (var session in sessions)
{ {
var controllers = session.SessionControllers; var controllers = session.SessionControllers;
foreach (var controller in controllers) foreach (var controller in controllers)
{ {
yield return controller.SendMessage(name, messageId, data, controllers, cancellationToken); yield return controller.SendMessage(name, messageId, data, cancellationToken);
} }
} }
} }
@ -1762,7 +1761,7 @@ namespace Emby.Server.Implementations.Session
throw new ArgumentNullException(nameof(info)); throw new ArgumentNullException(nameof(info));
} }
var user = info.UserId.Equals(Guid.Empty) var user = info.UserId == Guid.Empty
? null ? null
: _userManager.GetUserById(info.UserId); : _userManager.GetUserById(info.UserId);

View File

@ -3,7 +3,6 @@ using System.Threading.Tasks;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Events; using MediaBrowser.Model.Events;
using MediaBrowser.Model.Serialization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -12,7 +11,7 @@ namespace Emby.Server.Implementations.Session
/// <summary> /// <summary>
/// Class SessionWebSocketListener /// Class SessionWebSocketListener
/// </summary> /// </summary>
public class SessionWebSocketListener : IWebSocketListener, IDisposable public sealed class SessionWebSocketListener : IWebSocketListener, IDisposable
{ {
/// <summary> /// <summary>
/// The _session manager /// The _session manager
@ -23,42 +22,41 @@ namespace Emby.Server.Implementations.Session
/// The _logger /// The _logger
/// </summary> /// </summary>
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ILoggerFactory _loggerFactory;
/// <summary>
/// The _dto service
/// </summary>
private readonly IJsonSerializer _json;
private readonly IHttpServer _httpServer; private readonly IHttpServer _httpServer;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class. /// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class.
/// </summary> /// </summary>
/// <param name="logger">The logger.</param>
/// <param name="sessionManager">The session manager.</param> /// <param name="sessionManager">The session manager.</param>
/// <param name="loggerFactory">The logger factory.</param> /// <param name="loggerFactory">The logger factory.</param>
/// <param name="json">The json.</param>
/// <param name="httpServer">The HTTP server.</param> /// <param name="httpServer">The HTTP server.</param>
public SessionWebSocketListener(ISessionManager sessionManager, ILoggerFactory loggerFactory, IJsonSerializer json, IHttpServer httpServer) public SessionWebSocketListener(
ILogger<SessionWebSocketListener> logger,
ISessionManager sessionManager,
ILoggerFactory loggerFactory,
IHttpServer httpServer)
{ {
_logger = logger;
_sessionManager = sessionManager; _sessionManager = sessionManager;
_logger = loggerFactory.CreateLogger(GetType().Name); _loggerFactory = loggerFactory;
_json = json;
_httpServer = httpServer; _httpServer = httpServer;
httpServer.WebSocketConnected += _serverManager_WebSocketConnected;
httpServer.WebSocketConnected += OnServerManagerWebSocketConnected;
} }
void _serverManager_WebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e) private void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
{ {
var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint); var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint.ToString());
if (session != null) if (session != null)
{ {
EnsureController(session, e.Argument); EnsureController(session, e.Argument);
} }
else else
{ {
_logger.LogWarning("Unable to determine session based on url: {0}", e.Argument.Url); _logger.LogWarning("Unable to determine session based on query string: {0}", e.Argument.QueryString);
} }
} }
@ -79,9 +77,10 @@ namespace Emby.Server.Implementations.Session
return _sessionManager.GetSessionByAuthenticationToken(token, deviceId, remoteEndpoint); return _sessionManager.GetSessionByAuthenticationToken(token, deviceId, remoteEndpoint);
} }
/// <inheritdoc />
public void Dispose() public void Dispose()
{ {
_httpServer.WebSocketConnected -= _serverManager_WebSocketConnected; _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected;
} }
/// <summary> /// <summary>
@ -94,7 +93,8 @@ namespace Emby.Server.Implementations.Session
private void EnsureController(SessionInfo session, IWebSocketConnection connection) private void EnsureController(SessionInfo session, IWebSocketConnection connection)
{ {
var controllerInfo = session.EnsureController<WebSocketController>(s => new WebSocketController(s, _logger, _sessionManager)); var controllerInfo = session.EnsureController<WebSocketController>(
s => new WebSocketController(_loggerFactory.CreateLogger<WebSocketController>(), s, _sessionManager));
var controller = (WebSocketController)controllerInfo.Item1; var controller = (WebSocketController)controllerInfo.Item1;
controller.AddWebSocket(connection); controller.AddWebSocket(connection);

View File

@ -1,3 +1,7 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -11,60 +15,63 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Session namespace Emby.Server.Implementations.Session
{ {
public class WebSocketController : ISessionController, IDisposable public sealed class WebSocketController : ISessionController, IDisposable
{ {
public SessionInfo Session { get; private set; }
public IReadOnlyList<IWebSocketConnection> Sockets { get; private set; }
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ISessionManager _sessionManager; private readonly ISessionManager _sessionManager;
private readonly SessionInfo _session;
public WebSocketController(SessionInfo session, ILogger logger, ISessionManager sessionManager) private readonly List<IWebSocketConnection> _sockets;
private bool _disposed = false;
public WebSocketController(
ILogger<WebSocketController> logger,
SessionInfo session,
ISessionManager sessionManager)
{ {
Session = session;
_logger = logger; _logger = logger;
_session = session;
_sessionManager = sessionManager; _sessionManager = sessionManager;
Sockets = new List<IWebSocketConnection>(); _sockets = new List<IWebSocketConnection>();
} }
private bool HasOpenSockets => GetActiveSockets().Any(); private bool HasOpenSockets => GetActiveSockets().Any();
/// <inheritdoc />
public bool SupportsMediaControl => HasOpenSockets; public bool SupportsMediaControl => HasOpenSockets;
/// <inheritdoc />
public bool IsSessionActive => HasOpenSockets; public bool IsSessionActive => HasOpenSockets;
private IEnumerable<IWebSocketConnection> GetActiveSockets() private IEnumerable<IWebSocketConnection> GetActiveSockets()
{ => _sockets.Where(i => i.State == WebSocketState.Open);
return Sockets
.OrderByDescending(i => i.LastActivityDate)
.Where(i => i.State == WebSocketState.Open);
}
public void AddWebSocket(IWebSocketConnection connection) public void AddWebSocket(IWebSocketConnection connection)
{ {
var sockets = Sockets.ToList(); _logger.LogDebug("Adding websocket to session {Session}", _session.Id);
sockets.Add(connection); _sockets.Add(connection);
Sockets = sockets; connection.Closed += OnConnectionClosed;
connection.Closed += connection_Closed;
} }
void connection_Closed(object sender, EventArgs e) private void OnConnectionClosed(object sender, EventArgs e)
{ {
var connection = (IWebSocketConnection)sender; var connection = (IWebSocketConnection)sender;
var sockets = Sockets.ToList(); _logger.LogDebug("Removing websocket from session {Session}", _session.Id);
sockets.Remove(connection); _sockets.Remove(connection);
connection.Closed -= OnConnectionClosed;
Sockets = sockets; _sessionManager.CloseIfNeeded(_session);
_sessionManager.CloseIfNeeded(Session);
} }
public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken) /// <inheritdoc />
public Task SendMessage<T>(
string name,
Guid messageId,
T data,
CancellationToken cancellationToken)
{ {
var socket = GetActiveSockets() var socket = GetActiveSockets()
.OrderByDescending(i => i.LastActivityDate)
.FirstOrDefault(); .FirstOrDefault();
if (socket == null) if (socket == null)
@ -72,21 +79,30 @@ namespace Emby.Server.Implementations.Session
return Task.CompletedTask; return Task.CompletedTask;
} }
return socket.SendAsync(new WebSocketMessage<T> return socket.SendAsync(
new WebSocketMessage<T>
{ {
Data = data, Data = data,
MessageType = name, MessageType = name,
MessageId = messageId MessageId = messageId
},
}, cancellationToken); cancellationToken);
} }
/// <inheritdoc />
public void Dispose() public void Dispose()
{ {
foreach (var socket in Sockets.ToList()) if (_disposed)
{ {
socket.Closed -= connection_Closed; return;
} }
foreach (var socket in _sockets)
{
socket.Closed -= OnConnectionClosed;
}
_disposed = true;
} }
} }
} }

View File

@ -1,105 +0,0 @@
using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Net;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.SocketSharp
{
public class SharpWebSocket : IWebSocket
{
/// <summary>
/// The logger
/// </summary>
private readonly ILogger _logger;
public event EventHandler<EventArgs> Closed;
/// <summary>
/// Gets or sets the web socket.
/// </summary>
/// <value>The web socket.</value>
private readonly WebSocket _webSocket;
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private bool _disposed;
public SharpWebSocket(WebSocket socket, ILogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_webSocket = socket ?? throw new ArgumentNullException(nameof(socket));
}
/// <summary>
/// Gets the state.
/// </summary>
/// <value>The state.</value>
public WebSocketState State => _webSocket.State;
/// <summary>
/// Sends the async.
/// </summary>
/// <param name="bytes">The bytes.</param>
/// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken)
{
return _webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Binary, endOfMessage, cancellationToken);
}
/// <summary>
/// Sends the asynchronous.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken)
{
return _webSocket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes(text)), WebSocketMessageType.Text, endOfMessage, cancellationToken);
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool dispose)
{
if (_disposed)
{
return;
}
if (dispose)
{
_cancellationTokenSource.Cancel();
if (_webSocket.State == WebSocketState.Open)
{
_webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by client",
CancellationToken.None);
}
Closed?.Invoke(this, EventArgs.Empty);
}
_disposed = true;
}
/// <summary>
/// Gets or sets the receive action.
/// </summary>
/// <value>The receive action.</value>
public Action<byte[]> OnReceiveBytes { get; set; }
}
}

View File

@ -1,135 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.Net;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.SocketSharp
{
public class WebSocketSharpListener : IHttpListener
{
private readonly ILogger _logger;
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
private CancellationToken _disposeCancellationToken;
public WebSocketSharpListener(ILogger<WebSocketSharpListener> logger)
{
_logger = logger;
_disposeCancellationToken = _disposeCancellationTokenSource.Token;
}
public Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; }
public Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; }
public Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
private static void LogRequest(ILogger logger, HttpRequest request)
{
var url = request.GetDisplayUrl();
logger.LogInformation("WS {Url}. UserAgent: {UserAgent}", url, request.Headers[HeaderNames.UserAgent].ToString());
}
public async Task ProcessWebSocketRequest(HttpContext ctx)
{
try
{
LogRequest(_logger, ctx.Request);
var endpoint = ctx.Connection.RemoteIpAddress.ToString();
var url = ctx.Request.GetDisplayUrl();
var webSocketContext = await ctx.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false);
var socket = new SharpWebSocket(webSocketContext, _logger);
WebSocketConnected(new WebSocketConnectEventArgs
{
Url = url,
QueryString = ctx.Request.Query,
WebSocket = socket,
Endpoint = endpoint
});
WebSocketReceiveResult result;
var message = new List<byte>();
do
{
var buffer = WebSocket.CreateServerBuffer(4096);
result = await webSocketContext.ReceiveAsync(buffer, _disposeCancellationToken);
message.AddRange(buffer.Array.Take(result.Count));
if (result.EndOfMessage)
{
socket.OnReceiveBytes(message.ToArray());
message.Clear();
}
} while (socket.State == WebSocketState.Open && result.MessageType != WebSocketMessageType.Close);
if (webSocketContext.State == WebSocketState.Open)
{
await webSocketContext.CloseAsync(
result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
result.CloseStatusDescription,
_disposeCancellationToken).ConfigureAwait(false);
}
socket.Dispose();
}
catch (Exception ex)
{
_logger.LogError(ex, "AcceptWebSocketAsync error");
if (!ctx.Response.HasStarted)
{
ctx.Response.StatusCode = 500;
}
}
}
public Task Stop()
{
_disposeCancellationTokenSource.Cancel();
return Task.CompletedTask;
}
/// <summary>
/// Releases the unmanaged resources and disposes of the managed resources used.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private bool _disposed;
/// <summary>
/// Releases the unmanaged resources and disposes of the managed resources used.
/// </summary>
/// <param name="disposing">Whether or not the managed resources should be disposed.</param>
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
Stop().GetAwaiter().GetResult();
}
_disposed = true;
}
}
}

View File

@ -1,10 +0,0 @@
using System.Threading.Tasks;
using MediaBrowser.Model.Net;
namespace Emby.Server.Implementations.WebSockets
{
public interface IWebSocketHandler
{
Task ProcessMessage(WebSocketMessage<object> message, TaskCompletionSource<bool> taskCompletionSource);
}
}

View File

@ -1,102 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
using UtfUnknown;
namespace Emby.Server.Implementations.WebSockets
{
public class WebSocketManager
{
private readonly IWebSocketHandler[] _webSocketHandlers;
private readonly IJsonSerializer _jsonSerializer;
private readonly ILogger<WebSocketManager> _logger;
private const int BufferSize = 4096;
public WebSocketManager(IWebSocketHandler[] webSocketHandlers, IJsonSerializer jsonSerializer, ILogger<WebSocketManager> logger)
{
_webSocketHandlers = webSocketHandlers;
_jsonSerializer = jsonSerializer;
_logger = logger;
}
public async Task OnWebSocketConnected(WebSocket webSocket)
{
var taskCompletionSource = new TaskCompletionSource<bool>();
var cancellationToken = new CancellationTokenSource().Token;
WebSocketReceiveResult result;
var message = new List<byte>();
// Keep listening for incoming messages, otherwise the socket closes automatically
do
{
var buffer = WebSocket.CreateServerBuffer(BufferSize);
result = await webSocket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false);
message.AddRange(buffer.Array.Take(result.Count));
if (result.EndOfMessage)
{
await ProcessMessage(message.ToArray(), taskCompletionSource).ConfigureAwait(false);
message.Clear();
}
} while (!taskCompletionSource.Task.IsCompleted &&
webSocket.State == WebSocketState.Open &&
result.MessageType != WebSocketMessageType.Close);
if (webSocket.State == WebSocketState.Open)
{
await webSocket.CloseAsync(
result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
result.CloseStatusDescription,
cancellationToken).ConfigureAwait(false);
}
}
private async Task ProcessMessage(byte[] messageBytes, TaskCompletionSource<bool> taskCompletionSource)
{
var charset = CharsetDetector.DetectFromBytes(messageBytes).Detected?.EncodingName;
var message = string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase)
? Encoding.UTF8.GetString(messageBytes, 0, messageBytes.Length)
: Encoding.ASCII.GetString(messageBytes, 0, messageBytes.Length);
// All messages are expected to be valid JSON objects
if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase))
{
_logger.LogDebug("Received web socket message that is not a json structure: {Message}", message);
return;
}
try
{
var info = _jsonSerializer.DeserializeFromString<WebSocketMessage<object>>(message);
_logger.LogDebug("Websocket message received: {0}", info.MessageType);
var tasks = _webSocketHandlers.Select(handler => Task.Run(() =>
{
try
{
handler.ProcessMessage(info, taskCompletionSource).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "{HandlerType} failed processing WebSocket message {MessageType}",
handler.GetType().Name, info.MessageType ?? string.Empty);
}
}));
await Task.WhenAll(tasks);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing web socket message");
}
}
}
}

View File

@ -64,7 +64,6 @@ namespace Jellyfin.Server
app.UseResponseCompression(); app.UseResponseCompression();
// TODO app.UseMiddleware<WebSocketMiddleware>(); // TODO app.UseMiddleware<WebSocketMiddleware>();
app.Use(serverApplicationHost.ExecuteWebsocketHandlerAsync);
// TODO use when old API is removed: app.UseAuthentication(); // TODO use when old API is removed: app.UseAuthentication();
app.UseJellyfinApiSwagger(); app.UseJellyfinApiSwagger();

View File

@ -31,46 +31,46 @@ namespace MediaBrowser.Api.Sessions
{ {
_sessionManager = sessionManager; _sessionManager = sessionManager;
_sessionManager.SessionStarted += _sessionManager_SessionStarted; _sessionManager.SessionStarted += OnSessionManagerSessionStarted;
_sessionManager.SessionEnded += _sessionManager_SessionEnded; _sessionManager.SessionEnded += OnSessionManagerSessionEnded;
_sessionManager.PlaybackStart += _sessionManager_PlaybackStart; _sessionManager.PlaybackStart += OnSessionManagerPlaybackStart;
_sessionManager.PlaybackStopped += _sessionManager_PlaybackStopped; _sessionManager.PlaybackStopped += OnSessionManagerPlaybackStopped;
_sessionManager.PlaybackProgress += _sessionManager_PlaybackProgress; _sessionManager.PlaybackProgress += OnSessionManagerPlaybackProgress;
_sessionManager.CapabilitiesChanged += _sessionManager_CapabilitiesChanged; _sessionManager.CapabilitiesChanged += OnSessionManagerCapabilitiesChanged;
_sessionManager.SessionActivity += _sessionManager_SessionActivity; _sessionManager.SessionActivity += OnSessionManagerSessionActivity;
} }
void _sessionManager_SessionActivity(object sender, SessionEventArgs e) private void OnSessionManagerSessionActivity(object sender, SessionEventArgs e)
{ {
SendData(false); SendData(false);
} }
void _sessionManager_CapabilitiesChanged(object sender, SessionEventArgs e) private void OnSessionManagerCapabilitiesChanged(object sender, SessionEventArgs e)
{ {
SendData(true); SendData(true);
} }
void _sessionManager_PlaybackProgress(object sender, PlaybackProgressEventArgs e) private void OnSessionManagerPlaybackProgress(object sender, PlaybackProgressEventArgs e)
{ {
SendData(!e.IsAutomated); SendData(!e.IsAutomated);
} }
void _sessionManager_PlaybackStopped(object sender, PlaybackStopEventArgs e) private void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e)
{ {
SendData(true); SendData(true);
} }
void _sessionManager_PlaybackStart(object sender, PlaybackProgressEventArgs e) private void OnSessionManagerPlaybackStart(object sender, PlaybackProgressEventArgs e)
{ {
SendData(true); SendData(true);
} }
void _sessionManager_SessionEnded(object sender, SessionEventArgs e) private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e)
{ {
SendData(true); SendData(true);
} }
void _sessionManager_SessionStarted(object sender, SessionEventArgs e) private void OnSessionManagerSessionStarted(object sender, SessionEventArgs e)
{ {
SendData(true); SendData(true);
} }
@ -84,15 +84,16 @@ namespace MediaBrowser.Api.Sessions
return Task.FromResult(_sessionManager.Sessions); return Task.FromResult(_sessionManager.Sessions);
} }
/// <inheritdoc />
protected override void Dispose(bool dispose) protected override void Dispose(bool dispose)
{ {
_sessionManager.SessionStarted -= _sessionManager_SessionStarted; _sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
_sessionManager.SessionEnded -= _sessionManager_SessionEnded; _sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
_sessionManager.PlaybackStart -= _sessionManager_PlaybackStart; _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart;
_sessionManager.PlaybackStopped -= _sessionManager_PlaybackStopped; _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
_sessionManager.PlaybackProgress -= _sessionManager_PlaybackProgress; _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress;
_sessionManager.CapabilitiesChanged -= _sessionManager_CapabilitiesChanged; _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged;
_sessionManager.SessionActivity -= _sessionManager_SessionActivity; _sessionManager.SessionActivity -= OnSessionManagerSessionActivity;
base.Dispose(dispose); base.Dispose(dispose);
} }

View File

@ -1,4 +1,4 @@
using System.Collections.Generic; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Activity; using MediaBrowser.Model.Activity;
@ -10,7 +10,7 @@ namespace MediaBrowser.Api.System
/// <summary> /// <summary>
/// Class SessionInfoWebSocketListener /// Class SessionInfoWebSocketListener
/// </summary> /// </summary>
public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<List<ActivityLogEntry>, WebSocketListenerState> public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<ActivityLogEntry[], WebSocketListenerState>
{ {
/// <summary> /// <summary>
/// Gets the name. /// Gets the name.
@ -26,10 +26,10 @@ namespace MediaBrowser.Api.System
public ActivityLogWebSocketListener(ILogger<ActivityLogWebSocketListener> logger, IActivityManager activityManager) : base(logger) public ActivityLogWebSocketListener(ILogger<ActivityLogWebSocketListener> logger, IActivityManager activityManager) : base(logger)
{ {
_activityManager = activityManager; _activityManager = activityManager;
_activityManager.EntryCreated += _activityManager_EntryCreated; _activityManager.EntryCreated += OnEntryCreated;
} }
void _activityManager_EntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e) private void OnEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
{ {
SendData(true); SendData(true);
} }
@ -38,15 +38,15 @@ namespace MediaBrowser.Api.System
/// Gets the data to send. /// Gets the data to send.
/// </summary> /// </summary>
/// <returns>Task{SystemInfo}.</returns> /// <returns>Task{SystemInfo}.</returns>
protected override Task<List<ActivityLogEntry>> GetDataToSend() protected override Task<ActivityLogEntry[]> GetDataToSend()
{ {
return Task.FromResult(new List<ActivityLogEntry>()); return Task.FromResult(Array.Empty<ActivityLogEntry>());
} }
/// <inheritdoc />
protected override void Dispose(bool dispose) protected override void Dispose(bool dispose)
{ {
_activityManager.EntryCreated -= _activityManager_EntryCreated; _activityManager.EntryCreated -= OnEntryCreated;
base.Dispose(dispose); base.Dispose(dispose);
} }

View File

@ -116,7 +116,5 @@ namespace MediaBrowser.Controller
string ReverseVirtualPath(string path); string ReverseVirtualPath(string path);
Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next); Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next);
Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next);
} }
} }

View File

@ -77,8 +77,6 @@ namespace MediaBrowser.Controller.Net
return Task.CompletedTask; return Task.CompletedTask;
} }
protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
/// <summary> /// <summary>
/// Starts sending messages over a web socket /// Starts sending messages over a web socket
/// </summary> /// </summary>
@ -87,12 +85,12 @@ namespace MediaBrowser.Controller.Net
{ {
var vals = message.Data.Split(','); var vals = message.Data.Split(',');
var dueTimeMs = long.Parse(vals[0], UsCulture); var dueTimeMs = long.Parse(vals[0], CultureInfo.InvariantCulture);
var periodMs = long.Parse(vals[1], UsCulture); var periodMs = long.Parse(vals[1], CultureInfo.InvariantCulture);
var cancellationTokenSource = new CancellationTokenSource(); var cancellationTokenSource = new CancellationTokenSource();
Logger.LogDebug("{1} Begin transmitting over websocket to {0}", message.Connection.RemoteEndPoint, GetType().Name); Logger.LogDebug("WS {1} begin transmitting to {0}", message.Connection.RemoteEndPoint, GetType().Name);
var state = new TStateType var state = new TStateType
{ {
@ -154,7 +152,6 @@ namespace MediaBrowser.Controller.Net
{ {
MessageType = Name, MessageType = Name,
Data = data Data = data
}, cancellationToken).ConfigureAwait(false); }, cancellationToken).ConfigureAwait(false);
state.DateLastSendUtc = DateTime.UtcNow; state.DateLastSendUtc = DateTime.UtcNow;
@ -197,7 +194,7 @@ namespace MediaBrowser.Controller.Net
/// <param name="connection">The connection.</param> /// <param name="connection">The connection.</param>
private void DisposeConnection(Tuple<IWebSocketConnection, CancellationTokenSource, TStateType> connection) private void DisposeConnection(Tuple<IWebSocketConnection, CancellationTokenSource, TStateType> connection)
{ {
Logger.LogDebug("{1} stop transmitting over websocket to {0}", connection.Item1.RemoteEndPoint, GetType().Name); Logger.LogDebug("WS {1} stop transmitting to {0}", connection.Item1.RemoteEndPoint, GetType().Name);
// TODO disposing the connection seems to break websockets in subtle ways, so what is the purpose of this function really... // TODO disposing the connection seems to break websockets in subtle ways, so what is the purpose of this function really...
// connection.Item1.Dispose(); // connection.Item1.Dispose();
@ -242,6 +239,7 @@ namespace MediaBrowser.Controller.Net
public void Dispose() public void Dispose()
{ {
Dispose(true); Dispose(true);
GC.SuppressFinalize(this);
} }
} }

View File

@ -1,17 +1,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Model.Events; using MediaBrowser.Model.Events;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net namespace MediaBrowser.Controller.Net
{ {
/// <summary> /// <summary>
/// Interface IHttpServer /// Interface IHttpServer.
/// </summary> /// </summary>
public interface IHttpServer : IDisposable public interface IHttpServer
{ {
/// <summary> /// <summary>
/// Gets the URL prefix. /// Gets the URL prefix.
@ -19,11 +17,6 @@ namespace MediaBrowser.Controller.Net
/// <value>The URL prefix.</value> /// <value>The URL prefix.</value>
string[] UrlPrefixes { get; } string[] UrlPrefixes { get; }
/// <summary>
/// Stops this instance.
/// </summary>
void Stop();
/// <summary> /// <summary>
/// Occurs when [web socket connected]. /// Occurs when [web socket connected].
/// </summary> /// </summary>
@ -39,23 +32,11 @@ namespace MediaBrowser.Controller.Net
/// </summary> /// </summary>
string GlobalResponse { get; set; } string GlobalResponse { get; set; }
/// <summary>
/// Sends the http context to the socket listener
/// </summary>
/// <param name="ctx"></param>
/// <returns></returns>
Task ProcessWebSocketRequest(HttpContext ctx);
/// <summary> /// <summary>
/// The HTTP request handler /// The HTTP request handler
/// </summary> /// </summary>
/// <param name="httpReq"></param> /// <param name="context"></param>
/// <param name="urlString"></param>
/// <param name="host"></param>
/// <param name="localPath"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns> /// <returns></returns>
Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, Task RequestHandler(HttpContext context);
CancellationToken cancellationToken);
} }
} }

View File

@ -1,4 +1,7 @@
#nullable enable
using System; using System;
using System.Net;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -7,18 +10,12 @@ using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net namespace MediaBrowser.Controller.Net
{ {
public interface IWebSocketConnection : IDisposable public interface IWebSocketConnection
{ {
/// <summary> /// <summary>
/// Occurs when [closed]. /// Occurs when [closed].
/// </summary> /// </summary>
event EventHandler<EventArgs> Closed; event EventHandler<EventArgs>? Closed;
/// <summary>
/// Gets the id.
/// </summary>
/// <value>The id.</value>
Guid Id { get; }
/// <summary> /// <summary>
/// Gets the last activity date. /// Gets the last activity date.
@ -26,22 +23,17 @@ namespace MediaBrowser.Controller.Net
/// <value>The last activity date.</value> /// <value>The last activity date.</value>
DateTime LastActivityDate { get; } DateTime LastActivityDate { get; }
/// <summary>
/// Gets or sets the URL.
/// </summary>
/// <value>The URL.</value>
string Url { get; set; }
/// <summary> /// <summary>
/// Gets or sets the query string. /// Gets or sets the query string.
/// </summary> /// </summary>
/// <value>The query string.</value> /// <value>The query string.</value>
IQueryCollection QueryString { get; set; } IQueryCollection QueryString { get; }
/// <summary> /// <summary>
/// Gets or sets the receive action. /// Gets or sets the receive action.
/// </summary> /// </summary>
/// <value>The receive action.</value> /// <value>The receive action.</value>
Func<WebSocketMessageInfo, Task> OnReceive { get; set; } Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
/// <summary> /// <summary>
/// Gets the state. /// Gets the state.
@ -53,7 +45,7 @@ namespace MediaBrowser.Controller.Net
/// Gets the remote end point. /// Gets the remote end point.
/// </summary> /// </summary>
/// <value>The remote end point.</value> /// <value>The remote end point.</value>
string RemoteEndPoint { get; } IPAddress? RemoteEndPoint { get; }
/// <summary> /// <summary>
/// Sends a message asynchronously. /// Sends a message asynchronously.
@ -65,21 +57,6 @@ namespace MediaBrowser.Controller.Net
/// <exception cref="ArgumentNullException">message</exception> /// <exception cref="ArgumentNullException">message</exception>
Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken); Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken);
/// <summary> Task ProcessAsync(CancellationToken cancellationToken = default);
/// Sends a message asynchronously.
/// </summary>
/// <param name="buffer">The buffer.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task SendAsync(byte[] buffer, CancellationToken cancellationToken);
/// <summary>
/// Sends a message asynchronously.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
/// <exception cref="ArgumentNullException">buffer</exception>
Task SendAsync(string text, CancellationToken cancellationToken);
} }
} }

View File

@ -1,3 +1,4 @@
using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -20,6 +21,6 @@ namespace MediaBrowser.Controller.Session
/// <summary> /// <summary>
/// Sends the message. /// Sends the message.
/// </summary> /// </summary>
Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken); Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken);
} }
} }

View File

@ -10,13 +10,23 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.Session namespace MediaBrowser.Controller.Session
{ {
/// <summary> /// <summary>
/// Class SessionInfo /// Class SessionInfo.
/// </summary> /// </summary>
public class SessionInfo : IDisposable public sealed class SessionInfo : IDisposable
{ {
private ISessionManager _sessionManager; // 1 second
private const long ProgressIncrement = 10000000;
private readonly ISessionManager _sessionManager;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly object _progressLock = new object();
private Timer _progressTimer;
private PlaybackProgressInfo _lastProgressInfo;
private bool _disposed = false;
public SessionInfo(ISessionManager sessionManager, ILogger logger) public SessionInfo(ISessionManager sessionManager, ILogger logger)
{ {
_sessionManager = sessionManager; _sessionManager = sessionManager;
@ -97,8 +107,6 @@ namespace MediaBrowser.Controller.Session
/// <value>The name of the device.</value> /// <value>The name of the device.</value>
public string DeviceName { get; set; } public string DeviceName { get; set; }
public string DeviceType { get; set; }
/// <summary> /// <summary>
/// Gets or sets the now playing item. /// Gets or sets the now playing item.
/// </summary> /// </summary>
@ -128,22 +136,6 @@ namespace MediaBrowser.Controller.Session
[JsonIgnore] [JsonIgnore]
public ISessionController[] SessionControllers { get; set; } public ISessionController[] SessionControllers { get; set; }
/// <summary>
/// Gets or sets the supported commands.
/// </summary>
/// <value>The supported commands.</value>
public string[] SupportedCommands
{
get
{
if (Capabilities == null)
{
return new string[] { };
}
return Capabilities.SupportedCommands;
}
}
public TranscodingInfo TranscodingInfo { get; set; } public TranscodingInfo TranscodingInfo { get; set; }
/// <summary> /// <summary>
@ -215,6 +207,14 @@ namespace MediaBrowser.Controller.Session
} }
} }
public QueueItem[] NowPlayingQueue { get; set; }
public bool HasCustomDeviceName { get; set; }
public string PlaylistItemId { get; set; }
public string UserPrimaryImageTag { get; set; }
public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory) public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory)
{ {
var controllers = SessionControllers.ToList(); var controllers = SessionControllers.ToList();
@ -258,10 +258,6 @@ namespace MediaBrowser.Controller.Session
return false; return false;
} }
private readonly object _progressLock = new object();
private Timer _progressTimer;
private PlaybackProgressInfo _lastProgressInfo;
public void StartAutomaticProgress(PlaybackProgressInfo progressInfo) public void StartAutomaticProgress(PlaybackProgressInfo progressInfo)
{ {
if (_disposed) if (_disposed)
@ -284,9 +280,6 @@ namespace MediaBrowser.Controller.Session
} }
} }
// 1 second
private const long ProgressIncrement = 10000000;
private async void OnProgressTimerCallback(object state) private async void OnProgressTimerCallback(object state)
{ {
if (_disposed) if (_disposed)
@ -345,8 +338,7 @@ namespace MediaBrowser.Controller.Session
} }
} }
private bool _disposed = false; /// <inheritdoc />
public void Dispose() public void Dispose()
{ {
_disposed = true; _disposed = true;
@ -358,30 +350,12 @@ namespace MediaBrowser.Controller.Session
foreach (var controller in controllers) foreach (var controller in controllers)
{ {
var disposable = controller as IDisposable; if (controller is IDisposable disposable)
if (disposable != null)
{ {
_logger.LogDebug("Disposing session controller {0}", disposable.GetType().Name); _logger.LogDebug("Disposing session controller {0}", disposable.GetType().Name);
try
{
disposable.Dispose(); disposable.Dispose();
} }
catch (Exception ex)
{
_logger.LogError(ex, "Error disposing session controller");
} }
} }
} }
_sessionManager = null;
}
public QueueItem[] NowPlayingQueue { get; set; }
public bool HasCustomDeviceName { get; set; }
public string PlaylistItemId { get; set; }
public string ServerId { get; set; }
public string UserPrimaryImageTag { get; set; }
}
} }

View File

@ -1,5 +1,8 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
namespace MediaBrowser.Model.Net namespace MediaBrowser.Model.Net
{ {
/// <summary> /// <summary>
@ -13,7 +16,9 @@ namespace MediaBrowser.Model.Net
/// </summary> /// </summary>
/// <value>The type of the message.</value> /// <value>The type of the message.</value>
public string MessageType { get; set; } public string MessageType { get; set; }
public string MessageId { get; set; }
public Guid MessageId { get; set; }
public string ServerId { get; set; } public string ServerId { get; set; }
/// <summary> /// <summary>
@ -22,5 +27,4 @@ namespace MediaBrowser.Model.Net
/// <value>The data.</value> /// <value>The data.</value>
public T Data { get; set; } public T Data { get; set; }
} }
} }

View File

@ -27,9 +27,6 @@ namespace MediaBrowser.Providers.Tmdb.TV
public class TmdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder public class TmdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder
{ {
private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}?api_key={1}&append_to_response=credits,images,keywords,external_ids,videos,content_ratings"; private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}?api_key={1}&append_to_response=credits,images,keywords,external_ids,videos,content_ratings";
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
internal static TmdbSeriesProvider Current { get; private set; }
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
@ -39,6 +36,10 @@ namespace MediaBrowser.Providers.Tmdb.TV
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
internal static TmdbSeriesProvider Current { get; private set; }
public TmdbSeriesProvider( public TmdbSeriesProvider(
IJsonSerializer jsonSerializer, IJsonSerializer jsonSerializer,
IFileSystem fileSystem, IFileSystem fileSystem,
@ -217,10 +218,9 @@ namespace MediaBrowser.Providers.Tmdb.TV
var series = seriesResult.Item; var series = seriesResult.Item;
series.Name = seriesInfo.Name; series.Name = seriesInfo.Name;
series.OriginalTitle = seriesInfo.Original_Name;
series.SetProviderId(MetadataProviders.Tmdb, seriesInfo.Id.ToString(_usCulture)); series.SetProviderId(MetadataProviders.Tmdb, seriesInfo.Id.ToString(_usCulture));
//series.VoteCount = seriesInfo.vote_count;
string voteAvg = seriesInfo.Vote_Average.ToString(CultureInfo.InvariantCulture); string voteAvg = seriesInfo.Vote_Average.ToString(CultureInfo.InvariantCulture);
if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out float rating)) if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out float rating))
@ -240,7 +240,7 @@ namespace MediaBrowser.Providers.Tmdb.TV
series.Genres = seriesInfo.Genres.Select(i => i.Name).ToArray(); series.Genres = seriesInfo.Genres.Select(i => i.Name).ToArray();
} }
//series.HomePageUrl = seriesInfo.homepage; series.HomePageUrl = seriesInfo.Homepage;
series.RunTimeTicks = seriesInfo.Episode_Run_Time.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault(); series.RunTimeTicks = seriesInfo.Episode_Run_Time.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault();
@ -308,7 +308,9 @@ namespace MediaBrowser.Providers.Tmdb.TV
seriesResult.ResetPeople(); seriesResult.ResetPeople();
var tmdbImageUrl = settings.images.GetImageUrl("original"); var tmdbImageUrl = settings.images.GetImageUrl("original");
if (seriesInfo.Credits != null && seriesInfo.Credits.Cast != null) if (seriesInfo.Credits != null)
{
if (seriesInfo.Credits.Cast != null)
{ {
foreach (var actor in seriesInfo.Credits.Cast.OrderBy(a => a.Order)) foreach (var actor in seriesInfo.Credits.Cast.OrderBy(a => a.Order))
{ {
@ -333,6 +335,36 @@ namespace MediaBrowser.Providers.Tmdb.TV
seriesResult.AddPerson(personInfo); seriesResult.AddPerson(personInfo);
} }
} }
if (seriesInfo.Credits.Crew != null)
{
var keepTypes = new[]
{
PersonType.Director,
PersonType.Writer,
PersonType.Producer
};
foreach (var person in seriesInfo.Credits.Crew)
{
// Normalize this
var type = TmdbUtils.MapCrewToPersonType(person);
if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
&& !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
{
continue;
}
seriesResult.AddPerson(new PersonInfo
{
Name = person.Name.Trim(),
Role = person.Job,
Type = type
});
}
}
}
} }
internal static string GetSeriesDataPath(IApplicationPaths appPaths, string tmdbId) internal static string GetSeriesDataPath(IApplicationPaths appPaths, string tmdbId)

View File

@ -4,18 +4,51 @@ using MediaBrowser.Providers.Tmdb.Models.General;
namespace MediaBrowser.Providers.Tmdb namespace MediaBrowser.Providers.Tmdb
{ {
/// <summary>
/// Utilities for the TMDb provider
/// </summary>
public static class TmdbUtils public static class TmdbUtils
{ {
/// <summary>
/// URL of the TMDB instance to use.
/// </summary>
public const string BaseTmdbUrl = "https://www.themoviedb.org/"; public const string BaseTmdbUrl = "https://www.themoviedb.org/";
/// <summary>
/// URL of the TMDB API instance to use.
/// </summary>
public const string BaseTmdbApiUrl = "https://api.themoviedb.org/"; public const string BaseTmdbApiUrl = "https://api.themoviedb.org/";
/// <summary>
/// Name of the provider.
/// </summary>
public const string ProviderName = "TheMovieDb"; public const string ProviderName = "TheMovieDb";
/// <summary>
/// API key to use when performing an API call.
/// </summary>
public const string ApiKey = "4219e299c89411838049ab0dab19ebd5"; public const string ApiKey = "4219e299c89411838049ab0dab19ebd5";
/// <summary>
/// Value of the Accept header for requests to the provider.
/// </summary>
public const string AcceptHeader = "application/json,image/*"; public const string AcceptHeader = "application/json,image/*";
/// <summary>
/// Maps the TMDB provided roles for crew members to Jellyfin roles.
/// </summary>
/// <param name="crew">Crew member to map against the Jellyfin person types.</param>
/// <returns>The Jellyfin person type.</returns>
public static string MapCrewToPersonType(Crew crew) public static string MapCrewToPersonType(Crew crew)
{ {
if (crew.Department.Equals("production", StringComparison.InvariantCultureIgnoreCase) if (crew.Department.Equals("production", StringComparison.InvariantCultureIgnoreCase)
&& crew.Job.IndexOf("producer", StringComparison.InvariantCultureIgnoreCase) != -1) && crew.Job.Contains("director", StringComparison.InvariantCultureIgnoreCase))
{
return PersonType.Director;
}
if (crew.Department.Equals("production", StringComparison.InvariantCultureIgnoreCase)
&& crew.Job.Contains("producer", StringComparison.InvariantCultureIgnoreCase))
{ {
return PersonType.Producer; return PersonType.Producer;
} }