From 976459d3e8a8b889cebc2cf281e38b0fbc19c9b9 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Tue, 17 Dec 2019 23:15:02 +0100 Subject: Rewrite WebSocket handling code --- Emby.Server.Implementations/ApplicationHost.cs | 34 +++----------------------- 1 file changed, 3 insertions(+), 31 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 0bb1d832f..e3e071af9 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -45,7 +45,6 @@ using Emby.Server.Implementations.ScheduledTasks; using Emby.Server.Implementations.Security; using Emby.Server.Implementations.Serialization; using Emby.Server.Implementations.Session; -using Emby.Server.Implementations.SocketSharp; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; using MediaBrowser.Api; @@ -105,7 +104,6 @@ using MediaBrowser.Providers.TV.TheTVDB; using MediaBrowser.WebDashboard.Api; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -629,32 +627,8 @@ namespace Emby.Server.Implementations } } - public async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func next) - { - if (!context.WebSockets.IsWebSocketRequest) - { - await next().ConfigureAwait(false); - return; - } - - await HttpServer.ProcessWebSocketRequest(context).ConfigureAwait(false); - } - - public async Task ExecuteHttpHandlerAsync(HttpContext context, Func 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, Logger); - await HttpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted).ConfigureAwait(false); - } + public Task ExecuteHttpHandlerAsync(HttpContext context, Func next) + => HttpServer.RequestHandler(context); /// /// Registers resources that classes will depend on @@ -777,7 +751,7 @@ namespace Emby.Server.Implementations NetworkManager, JsonSerializer, XmlSerializer, - CreateHttpListener()) + LoggerFactory) { GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading") }; @@ -1194,8 +1168,6 @@ namespace Emby.Server.Implementations }); } - protected IHttpListener CreateHttpListener() => new WebSocketSharpListener(Logger); - private CertificateInfo GetCertificateInfo(bool generateCertificate) { // Custom cert -- cgit v1.2.3 From ca71ac72abf5d5ff31d50553283a43de298e0c73 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Thu, 2 Apr 2020 17:45:04 -0400 Subject: Replace EnableHttps and SupportsHttps with ListenWithHttps and CanConnectWithHttps --- Emby.Server.Implementations/ApplicationHost.cs | 15 ++++++++++----- .../EntryPoints/ExternalPortForwarding.cs | 2 +- .../HttpServer/HttpListenerHost.cs | 2 +- Jellyfin.Server/Program.cs | 4 ++-- MediaBrowser.Controller/IServerApplicationHost.cs | 5 ++--- MediaBrowser.Model/System/SystemInfo.cs | 4 ++-- 6 files changed, 18 insertions(+), 14 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index c959cc974..8158b4559 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1408,7 +1408,7 @@ namespace Emby.Server.Implementations InternalMetadataPath = ApplicationPaths.InternalMetadataPath, CachePath = ApplicationPaths.CachePath, HttpServerPortNumber = HttpPort, - SupportsHttps = SupportsHttps, + SupportsHttps = CanConnectWithHttps, HttpsPortNumber = HttpsPort, OperatingSystem = OperatingSystem.Id.ToString(), OperatingSystemDisplayName = OperatingSystem.Name, @@ -1446,9 +1446,14 @@ namespace Emby.Server.Implementations }; } - public bool EnableHttps => SupportsHttps && ServerConfigurationManager.Configuration.EnableHttps; + /// + public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.Configuration.EnableHttps; - public bool SupportsHttps => Certificate != null || ServerConfigurationManager.Configuration.IsBehindProxy; + /// + /// Gets a value indicating whether a client can connect to the server over HTTPS, either directly or via a + /// reverse proxy. + /// + public bool CanConnectWithHttps => ListenWithHttps || ServerConfigurationManager.Configuration.IsBehindProxy; public async Task GetLocalApiUrl(CancellationToken cancellationToken) { @@ -1509,10 +1514,10 @@ namespace Emby.Server.Implementations public string GetLocalApiUrl(ReadOnlySpan host) { var url = new StringBuilder(64); - url.Append(EnableHttps ? "https://" : "http://") + url.Append(ListenWithHttps ? "https://" : "http://") .Append(host) .Append(':') - .Append(EnableHttps ? HttpsPort : HttpPort); + .Append(ListenWithHttps ? HttpsPort : HttpPort); string baseUrl = ServerConfigurationManager.Configuration.BaseUrl; if (baseUrl.Length != 0) diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index e290c62e1..2023c470a 100644 --- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.EntryPoints .Append(config.PublicPort).Append(Separator) .Append(_appHost.HttpPort).Append(Separator) .Append(_appHost.HttpsPort).Append(Separator) - .Append(_appHost.EnableHttps).Append(Separator) + .Append(_appHost.ListenWithHttps).Append(Separator) .Append(config.EnableRemoteAccess).Append(Separator) .ToString(); } diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 7a812f320..e3f8ec014 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -422,7 +422,7 @@ namespace Emby.Server.Implementations.HttpServer private bool ValidateSsl(string remoteIp, string urlString) { - if (_config.Configuration.RequireHttps && _appHost.EnableHttps && !_config.Configuration.IsBehindProxy) + if (_config.Configuration.RequireHttps && _appHost.ListenWithHttps) { if (urlString.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == -1) { diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 4abdd59aa..ddd1054ad 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -274,7 +274,7 @@ namespace Jellyfin.Server _logger.LogInformation("Kestrel listening on {IpAddress}", address); options.Listen(address, appHost.HttpPort); - if (appHost.EnableHttps && appHost.Certificate != null) + if (appHost.ListenWithHttps) { options.Listen(address, appHost.HttpsPort, listenOptions => { @@ -289,7 +289,7 @@ namespace Jellyfin.Server _logger.LogInformation("Kestrel listening on all interfaces"); options.ListenAnyIP(appHost.HttpPort); - if (appHost.EnableHttps && appHost.Certificate != null) + if (appHost.ListenWithHttps) { options.ListenAnyIP(appHost.HttpsPort, listenOptions => { diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 608ffc61c..d999f76db 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -39,10 +39,9 @@ namespace MediaBrowser.Controller int HttpsPort { get; } /// - /// Gets a value indicating whether [supports HTTPS]. + /// Gets a value indicating whether the server should listen on an HTTPS port. /// - /// true if [supports HTTPS]; otherwise, false. - bool EnableHttps { get; } + bool ListenWithHttps { get; } /// /// Gets a value indicating whether this instance has update available. diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index cfa7684c9..83c60563e 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -124,9 +124,9 @@ namespace MediaBrowser.Model.System public int HttpServerPortNumber { get; set; } /// - /// Gets or sets a value indicating whether [enable HTTPS]. + /// Gets or sets a value indicating whether a client can connect to the server over HTTPS, either directly or + /// via a reverse proxy. /// - /// true if [enable HTTPS]; otherwise, false. public bool SupportsHttps { get; set; } /// -- cgit v1.2.3 From 410a322fe22302eb3c8a37ea38bbe1d7e9d12aff Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sun, 5 Apr 2020 23:30:57 -0400 Subject: Add CanConnectWithHttps to interface --- Emby.Server.Implementations/ApplicationHost.cs | 5 +---- MediaBrowser.Controller/IServerApplicationHost.cs | 6 ++++++ 2 files changed, 7 insertions(+), 4 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 8158b4559..9cb747171 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1449,10 +1449,7 @@ namespace Emby.Server.Implementations /// public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.Configuration.EnableHttps; - /// - /// Gets a value indicating whether a client can connect to the server over HTTPS, either directly or via a - /// reverse proxy. - /// + /// public bool CanConnectWithHttps => ListenWithHttps || ServerConfigurationManager.Configuration.IsBehindProxy; public async Task GetLocalApiUrl(CancellationToken cancellationToken) diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index d999f76db..7742279f7 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -43,6 +43,12 @@ namespace MediaBrowser.Controller /// bool ListenWithHttps { get; } + /// + /// Gets a value indicating whether a client can connect to the server over HTTPS, either directly or via a + /// reverse proxy. + /// + bool CanConnectWithHttps { get; } + /// /// Gets a value indicating whether this instance has update available. /// -- cgit v1.2.3 From 36f3e933a23d802d154c16fd304a82c3fe3f453d Mon Sep 17 00:00:00 2001 From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com> Date: Wed, 15 Apr 2020 14:28:42 -0500 Subject: Add quick connect --- CONTRIBUTORS.md | 1 + Emby.Server.Implementations/ApplicationHost.cs | 3 + .../QuickConnect/QuickConnectManager.cs | 262 +++++++++++++++++++++ .../Session/SessionManager.cs | 18 ++ .../QuickConnect/QuickConnectService.cs | 145 ++++++++++++ MediaBrowser.Api/UserService.cs | 34 +++ .../QuickConnect/IQuickConnect.cs | 91 +++++++ MediaBrowser.Controller/Session/ISessionManager.cs | 2 + .../QuickConnect/QuickConnectResult.cs | 50 ++++ .../QuickConnect/QuickConnectResultDto.cs | 53 +++++ .../QuickConnect/QuickConnectState.cs | 23 ++ 11 files changed, 682 insertions(+) create mode 100644 Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs create mode 100644 MediaBrowser.Api/QuickConnect/QuickConnectService.cs create mode 100644 MediaBrowser.Controller/QuickConnect/IQuickConnect.cs create mode 100644 MediaBrowser.Model/QuickConnect/QuickConnectResult.cs create mode 100644 MediaBrowser.Model/QuickConnect/QuickConnectResultDto.cs create mode 100644 MediaBrowser.Model/QuickConnect/QuickConnectState.cs (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index ce956176e..edd33a2fb 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -15,6 +15,7 @@ - [bugfixin](https://github.com/bugfixin) - [chaosinnovator](https://github.com/chaosinnovator) - [ckcr4lyf](https://github.com/ckcr4lyf) + - [ConfusedPolarBear](https://github.com/ConfusedPolarBear) - [crankdoofus](https://github.com/crankdoofus) - [crobibero](https://github.com/crobibero) - [cromefire](https://github.com/cromefire) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 81a80ddb2..de044a4aa 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -74,6 +74,7 @@ using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.QuickConnect; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; @@ -857,6 +858,8 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(typeof(IAttachmentExtractor), typeof(MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor)); + serviceCollection.AddSingleton(typeof(IQuickConnect), typeof(QuickConnect.QuickConnectManager)); + _displayPreferencesRepository.Initialize(); var userDataRepo = new SqliteUserDataRepository(LoggerFactory.CreateLogger(), ApplicationPaths); diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs new file mode 100644 index 000000000..30418097c --- /dev/null +++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Security.Cryptography; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.QuickConnect; +using MediaBrowser.Controller.Security; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.QuickConnect; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Services; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.QuickConnect +{ + /// + /// Quick connect implementation. + /// + public class QuickConnectManager : IQuickConnect + { + private readonly RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider(); + private Dictionary _currentRequests = new Dictionary(); + + private ILogger _logger; + private IUserManager _userManager; + private ILocalizationManager _localizationManager; + private IJsonSerializer _jsonSerializer; + private IAuthenticationRepository _authenticationRepository; + private IAuthorizationContext _authContext; + private IServerApplicationHost _appHost; + + /// + /// Initializes a new instance of the class. + /// Should only be called at server startup when a singleton is created. + /// + /// Logger. + /// User manager. + /// Localization. + /// JSON serializer. + /// Application host. + /// Authentication context. + /// Authentication repository. + public QuickConnectManager( + ILoggerFactory loggerFactory, + IUserManager userManager, + ILocalizationManager localization, + IJsonSerializer jsonSerializer, + IServerApplicationHost appHost, + IAuthorizationContext authContext, + IAuthenticationRepository authenticationRepository) + { + _logger = loggerFactory.CreateLogger(nameof(QuickConnectManager)); + _userManager = userManager; + _localizationManager = localization; + _jsonSerializer = jsonSerializer; + _appHost = appHost; + _authContext = authContext; + _authenticationRepository = authenticationRepository; + } + + /// + public int CodeLength { get; set; } = 6; + + /// + public string TokenNamePrefix { get; set; } = "QuickConnect-"; + + /// + public QuickConnectState State { get; private set; } = QuickConnectState.Unavailable; + + /// + public int RequestExpiry { get; set; } = 30; + + /// + public void AssertActive() + { + if (State != QuickConnectState.Active) + { + throw new InvalidOperationException("Quick connect is not active on this server"); + } + } + + /// + public void SetEnabled(QuickConnectState newState) + { + _logger.LogDebug("Changed quick connect state from {0} to {1}", State, newState); + + State = newState; + } + + /// + public QuickConnectResult TryConnect(string friendlyName) + { + if (State != QuickConnectState.Active) + { + _logger.LogDebug("Refusing quick connect initiation request, current state is {0}", State); + + return new QuickConnectResult() + { + Error = "Quick connect is not active on this server" + }; + } + + _logger.LogDebug("Got new quick connect request from {friendlyName}", friendlyName); + + var lookup = GenerateSecureRandom(); + var result = new QuickConnectResult() + { + Lookup = lookup, + Secret = GenerateSecureRandom(), + FriendlyName = friendlyName, + DateAdded = DateTime.Now, + Code = GenerateCode() + }; + + _currentRequests[lookup] = result; + return result; + } + + /// + public QuickConnectResult CheckRequestStatus(string secret) + { + AssertActive(); + ExpireRequests(); + + string lookup = _currentRequests.Where(x => x.Value.Secret == secret).Select(x => x.Value.Lookup).DefaultIfEmpty(string.Empty).First(); + + _logger.LogDebug("Transformed private identifier {0} into public lookup {1}", secret, lookup); + + if (!_currentRequests.ContainsKey(lookup)) + { + throw new KeyNotFoundException("Unable to find request with provided identifier"); + } + + return _currentRequests[lookup]; + } + + /// + public List GetCurrentRequests() + { + return GetCurrentRequestsInternal().Select(x => (QuickConnectResultDto)x).ToList(); + } + + /// + public List GetCurrentRequestsInternal() + { + AssertActive(); + ExpireRequests(); + return _currentRequests.Values.ToList(); + } + + /// + public string GenerateCode() + { + // TODO: output may be biased + + int min = (int)Math.Pow(10, CodeLength - 1); + int max = (int)Math.Pow(10, CodeLength); + + uint scale = uint.MaxValue; + while (scale == uint.MaxValue) + { + byte[] raw = new byte[4]; + _rng.GetBytes(raw); + scale = BitConverter.ToUInt32(raw, 0); + } + + int code = (int)(min + (max - min) * (scale / (double)uint.MaxValue)); + return code.ToString(CultureInfo.InvariantCulture); + } + + /// + public bool AuthorizeRequest(IRequest request, string lookup) + { + AssertActive(); + + var auth = _authContext.GetAuthorizationInfo(request); + + ExpireRequests(); + + if (!_currentRequests.ContainsKey(lookup)) + { + throw new KeyNotFoundException("Unable to find request"); + } + + QuickConnectResult result = _currentRequests[lookup]; + + if (result.Authenticated) + { + throw new InvalidOperationException("Request is already authorized"); + } + + result.Authentication = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); + + // Advance the time on the request so it expires sooner as the client will pick up the changes in a few seconds + result.DateAdded = result.DateAdded.Subtract(new TimeSpan(0, RequestExpiry - 1, 0)); + + _authenticationRepository.Create(new AuthenticationInfo + { + AppName = TokenNamePrefix + result.FriendlyName, + AccessToken = result.Authentication, + DateCreated = DateTime.UtcNow, + DeviceId = _appHost.SystemId, + DeviceName = _appHost.FriendlyName, + AppVersion = _appHost.ApplicationVersionString, + UserId = auth.UserId + }); + + return true; + } + + /// + public int DeleteAllDevices(Guid user) + { + var raw = _authenticationRepository.Get(new AuthenticationInfoQuery() + { + DeviceId = _appHost.SystemId, + UserId = user + }); + + var tokens = raw.Items.Where(x => x.AppName.StartsWith(TokenNamePrefix, StringComparison.CurrentCulture)); + + foreach (var token in tokens) + { + _authenticationRepository.Delete(token); + _logger.LogDebug("Deleted token {0}", token.AccessToken); + } + + return tokens.Count(); + } + + private string GenerateSecureRandom(int length = 32) + { + var bytes = new byte[length]; + _rng.GetBytes(bytes); + + return string.Join(string.Empty, bytes.Select(x => x.ToString("x2", CultureInfo.InvariantCulture))); + } + + private void ExpireRequests() + { + var delete = new List(); + var values = _currentRequests.Values.ToList(); + + for (int i = 0; i < _currentRequests.Count; i++) + { + if (DateTime.Now > values[i].DateAdded.AddMinutes(RequestExpiry)) + { + delete.Add(values[i].Lookup); + } + } + + foreach (var lookup in delete) + { + _logger.LogDebug("Removing expired request {0}", lookup); + _currentRequests.Remove(lookup); + } + } + } +} diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index de768333d..2c8b2f29d 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1386,6 +1386,24 @@ namespace Emby.Server.Implementations.Session return AuthenticateNewSessionInternal(request, false); } + public Task AuthenticateQuickConnect(AuthenticationRequest request, string token) + { + var result = _authRepo.Get(new AuthenticationInfoQuery() + { + AccessToken = token, + DeviceId = _appHost.SystemId, + Limit = 1 + }); + + if(result.TotalRecordCount < 1) + { + throw new SecurityException("Unknown quick connect token"); + } + + request.UserId = result.Items[0].UserId; + return AuthenticateNewSessionInternal(request, false); + } + private async Task AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword) { CheckDisposed(); diff --git a/MediaBrowser.Api/QuickConnect/QuickConnectService.cs b/MediaBrowser.Api/QuickConnect/QuickConnectService.cs new file mode 100644 index 000000000..889a78839 --- /dev/null +++ b/MediaBrowser.Api/QuickConnect/QuickConnectService.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.QuickConnect; +using MediaBrowser.Model.QuickConnect; +using MediaBrowser.Model.Services; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Api.QuickConnect +{ + [Route("/QuickConnect/Initiate", "GET", Summary = "Requests a new quick connect code")] + public class Initiate : IReturn + { + [ApiMember(Name = "FriendlyName", Description = "Device friendly name", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string FriendlyName { get; set; } + } + + [Route("/QuickConnect/Connect", "GET", Summary = "Attempts to retrieve authentication information")] + public class Connect : IReturn + { + [ApiMember(Name = "Secret", Description = "Quick connect secret", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] + public string Secret { get; set; } + } + + [Route("/QuickConnect/List", "GET", Summary = "Lists all quick connect requests")] + [Authenticated] + public class QuickConnectList : IReturn> + { + } + + [Route("/QuickConnect/Authorize", "POST", Summary = "Authorizes a pending quick connect request")] + [Authenticated] + public class Authorize : IReturn + { + [ApiMember(Name = "Lookup", Description = "Quick connect public lookup", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] + public string Lookup { get; set; } + } + + [Route("/QuickConnect/Deauthorize", "POST", Summary = "Deletes all quick connect authorization tokens for the current user")] + [Authenticated] + public class Deauthorize : IReturn + { + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public Guid UserId { get; set; } + } + + [Route("/QuickConnect/Status", "GET", Summary = "Gets the current quick connect state")] + public class QuickConnectStatus : IReturn + { + + } + + [Route("/QuickConnect/Available", "POST", Summary = "Enables or disables quick connect")] + [Authenticated(Roles = "Admin")] + public class Available : IReturn + { + [ApiMember(Name = "Status", Description = "New quick connect status", IsRequired = false, DataType = "QuickConnectState", ParameterType = "query", Verb = "GET")] + public QuickConnectState Status { get; set; } + } + + [Route("/QuickConnect/Activate", "POST", Summary = "Temporarily activates quick connect for the time period defined in the server configuration")] + [Authenticated] + public class Activate : IReturn + { + } + + public class QuickConnectService : BaseApiService + { + private IQuickConnect _quickConnect; + private IUserManager _userManager; + private IAuthorizationContext _authContext; + + public QuickConnectService( + ILogger logger, + IServerConfigurationManager serverConfigurationManager, + IHttpResultFactory httpResultFactory, + IUserManager userManager, + IAuthorizationContext authContext, + IQuickConnect quickConnect) + : base(logger, serverConfigurationManager, httpResultFactory) + { + _userManager = userManager; + _quickConnect = quickConnect; + _authContext = authContext; + } + + public object Get(Initiate request) + { + return _quickConnect.TryConnect(request.FriendlyName); + } + + public object Get(Connect request) + { + return _quickConnect.CheckRequestStatus(request.Secret); + } + + public object Get(QuickConnectList request) + { + return _quickConnect.GetCurrentRequests(); + } + + public object Get(QuickConnectStatus request) + { + return _quickConnect.State; + } + + public object Post(Deauthorize request) + { + AssertCanUpdateUser(_authContext, _userManager, request.UserId, true); + + return _quickConnect.DeleteAllDevices(request.UserId); + } + + public object Post(Authorize request) + { + bool result = _quickConnect.AuthorizeRequest(Request, request.Lookup); + + Logger.LogInformation("Result of authorizing quick connect {0}: {1}", request.Lookup[..10], result); + + return result; + } + + public object Post(Activate request) + { + if (_quickConnect.State == QuickConnectState.Available) + { + _quickConnect.SetEnabled(QuickConnectState.Active); + + string name = _authContext.GetAuthorizationInfo(Request).User.Name; + Logger.LogInformation("{name} enabled quick connect", name); + } + + return _quickConnect.State; + } + + public object Post(Available request) + { + _quickConnect.SetEnabled(request.Status); + + return _quickConnect.State; + } + } +} diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs index 401514349..ebcacd2a3 100644 --- a/MediaBrowser.Api/UserService.cs +++ b/MediaBrowser.Api/UserService.cs @@ -117,6 +117,17 @@ namespace MediaBrowser.Api public string Pw { get; set; } } + [Route("/Users/AuthenticateWithQuickConnect", "POST", Summary = "Authenticates a user")] + public class AuthenticateUserQuickConnect : IReturn + { + /// + /// Gets or sets the token. + /// + /// The token + [ApiMember(Name = "Token", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")] + public string Token { get; set; } + } + /// /// Class UpdateUserPassword /// @@ -430,6 +441,29 @@ namespace MediaBrowser.Api } } + public async Task Post(AuthenticateUserQuickConnect request) + { + var auth = _authContext.GetAuthorizationInfo(Request); + + try + { + var result = await _sessionMananger.AuthenticateQuickConnect(new AuthenticationRequest + { + App = auth.Client, + AppVersion = auth.Version, + DeviceId = auth.DeviceId, + DeviceName = auth.Device + }, request.Token).ConfigureAwait(false); + + return ToOptimizedResult(result); + } + catch (SecurityException e) + { + // rethrow adding IP address to message + throw new SecurityException($"[{Request.RemoteIp}] {e.Message}"); + } + } + /// /// Posts the specified request. /// diff --git a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs new file mode 100644 index 000000000..e4a790ffe --- /dev/null +++ b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.QuickConnect; +using MediaBrowser.Model.Services; + +namespace MediaBrowser.Controller.QuickConnect +{ + /// + /// Quick connect standard interface. + /// + public interface IQuickConnect + { + /// + /// Gets or sets the length of user facing codes. + /// + public int CodeLength { get; set; } + + /// + /// Gets or sets the string to prefix internal access tokens with. + /// + public string TokenNamePrefix { get; set; } + + /// + /// Gets the current state of quick connect. + /// + public QuickConnectState State { get; } + + /// + /// Gets or sets the time (in minutes) before a pending request will expire. + /// + public int RequestExpiry { get; set; } + + /// + /// Assert that quick connect is currently active and throws an exception if it is not. + /// + void AssertActive(); + + /// + /// Changes the status of quick connect. + /// + /// New state to change to + void SetEnabled(QuickConnectState newState); + + /// + /// Initiates a new quick connect request. + /// + /// Friendly device name to display in the request UI. + /// A quick connect result with tokens to proceed or a descriptive error message otherwise. + QuickConnectResult TryConnect(string friendlyName); + + /// + /// Checks the status of an individual request. + /// + /// Unique secret identifier of the request. + /// Quick connect result. + QuickConnectResult CheckRequestStatus(string secret); + + /// + /// Returns all current quick connect requests as DTOs. Does not include sensitive information. + /// + /// List of all quick connect results. + List GetCurrentRequests(); + + /// + /// Returns all current quick connect requests (including sensitive information). + /// + /// List of all quick connect results. + List GetCurrentRequestsInternal(); + + /// + /// Authorizes a quick connect request to connect as the calling user. + /// + /// HTTP request object. + /// Public request lookup value. + /// A boolean indicating if the authorization completed successfully. + bool AuthorizeRequest(IRequest request, string lookup); + + /// + /// Deletes all quick connect access tokens for the provided user. + /// + /// Guid of the user to delete tokens for. + /// A count of the deleted tokens. + int DeleteAllDevices(Guid user); + + /// + /// Generates a short code to display to the user to uniquely identify this request. + /// + /// A short, unique alphanumeric string. + string GenerateCode(); + } +} diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 771027103..74ffd5a18 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -246,6 +246,8 @@ namespace MediaBrowser.Controller.Session /// Task{SessionInfo}. Task AuthenticateNewSession(AuthenticationRequest request); + public Task AuthenticateQuickConnect(AuthenticationRequest request, string token); + /// /// Creates the new session. /// diff --git a/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs b/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs new file mode 100644 index 000000000..bc3fd0046 --- /dev/null +++ b/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs @@ -0,0 +1,50 @@ +using System; + +namespace MediaBrowser.Model.QuickConnect +{ + /// + /// Stores the result of an incoming quick connect request. + /// + public class QuickConnectResult + { + /// + /// Gets a value indicating whether this request is authorized. + /// + public bool Authenticated => !string.IsNullOrEmpty(Authentication); + + /// + /// Gets or sets the secret value used to uniquely identify this request. Can be used to retrieve authentication information. + /// + public string Secret { get; set; } + + /// + /// Gets or sets the public value used to uniquely identify this request. Can only be used to authorize the request. + /// + public string Lookup { get; set; } + + /// + /// Gets or sets the user facing code used so the user can quickly differentiate this request from others. + /// + public string Code { get; set; } + + /// + /// Gets or sets the device friendly name. + /// + public string FriendlyName { get; set; } + + /// + /// Gets or sets the private access token. + /// + public string Authentication { get; set; } + + /// + /// Gets or sets an error message. + /// + public string Error { get; set; } + + /// + /// Gets or sets the DateTime that this request was created. + /// + public DateTime DateAdded { get; set; } + } +} diff --git a/MediaBrowser.Model/QuickConnect/QuickConnectResultDto.cs b/MediaBrowser.Model/QuickConnect/QuickConnectResultDto.cs new file mode 100644 index 000000000..671b7cc94 --- /dev/null +++ b/MediaBrowser.Model/QuickConnect/QuickConnectResultDto.cs @@ -0,0 +1,53 @@ +using System; + +namespace MediaBrowser.Model.QuickConnect +{ + /// + /// Stores the non-sensitive results of an incoming quick connect request. + /// + public class QuickConnectResultDto + { + /// + /// Gets a value indicating whether this request is authorized. + /// + public bool Authenticated { get; private set; } + + /// + /// Gets the user facing code used so the user can quickly differentiate this request from others. + /// + public string Code { get; private set; } + + /// + /// Gets the public value used to uniquely identify this request. Can only be used to authorize the request. + /// + public string Lookup { get; private set; } + + /// + /// Gets the device friendly name. + /// + public string FriendlyName { get; private set; } + + /// + /// Gets the DateTime that this request was created. + /// + public DateTime DateAdded { get; private set; } + + /// + /// Cast an internal quick connect result to a DTO by removing all sensitive properties. + /// + /// QuickConnectResult object to cast + public static implicit operator QuickConnectResultDto(QuickConnectResult result) + { + QuickConnectResultDto resultDto = new QuickConnectResultDto + { + Authenticated = result.Authenticated, + Code = result.Code, + FriendlyName = result.FriendlyName, + DateAdded = result.DateAdded, + Lookup = result.Lookup + }; + + return resultDto; + } + } +} diff --git a/MediaBrowser.Model/QuickConnect/QuickConnectState.cs b/MediaBrowser.Model/QuickConnect/QuickConnectState.cs new file mode 100644 index 000000000..9f250519b --- /dev/null +++ b/MediaBrowser.Model/QuickConnect/QuickConnectState.cs @@ -0,0 +1,23 @@ +namespace MediaBrowser.Model.QuickConnect +{ + /// + /// Quick connect state. + /// + public enum QuickConnectState + { + /// + /// This feature has not been opted into and is unavailable until the server administrator chooses to opt-in. + /// + Unavailable, + + /// + /// The feature is enabled for use on the server but is not currently accepting connection requests. + /// + Available, + + /// + /// The feature is actively accepting connection requests. + /// + Active + } +} -- cgit v1.2.3 From fee76097f44d0f11356e4a559df9178063b1bf29 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Thu, 16 Apr 2020 21:45:00 -0400 Subject: Remove CanConnectWithHttps Property It is only used in one place and only adds confusion by existing --- Emby.Server.Implementations/ApplicationHost.cs | 4 +--- MediaBrowser.Controller/IServerApplicationHost.cs | 6 ------ 2 files changed, 1 insertion(+), 9 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 6a324e090..a39baa84a 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1413,7 +1413,7 @@ namespace Emby.Server.Implementations InternalMetadataPath = ApplicationPaths.InternalMetadataPath, CachePath = ApplicationPaths.CachePath, HttpServerPortNumber = HttpPort, - SupportsHttps = CanConnectWithHttps, + SupportsHttps = ListenWithHttps || ServerConfigurationManager.Configuration.IsBehindProxy, HttpsPortNumber = HttpsPort, OperatingSystem = OperatingSystem.Id.ToString(), OperatingSystemDisplayName = OperatingSystem.Name, @@ -1455,8 +1455,6 @@ namespace Emby.Server.Implementations public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.Configuration.EnableHttps; /// - public bool CanConnectWithHttps => ListenWithHttps || ServerConfigurationManager.Configuration.IsBehindProxy; - public async Task GetLocalApiUrl(CancellationToken cancellationToken) { try diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 7742279f7..d999f76db 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -43,12 +43,6 @@ namespace MediaBrowser.Controller /// bool ListenWithHttps { get; } - /// - /// Gets a value indicating whether a client can connect to the server over HTTPS, either directly or via a - /// reverse proxy. - /// - bool CanConnectWithHttps { get; } - /// /// Gets a value indicating whether this instance has update available. /// -- cgit v1.2.3 From 00a0e013c695a3741218985afadd31265a6ddb40 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Thu, 16 Apr 2020 21:46:49 -0400 Subject: Update documentation for URL methods in ApplicationHost --- Emby.Server.Implementations/ApplicationHost.cs | 6 +++--- MediaBrowser.Controller/IServerApplicationHost.cs | 24 +++++++++++++---------- 2 files changed, 17 insertions(+), 13 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index a39baa84a..7699faf14 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1510,7 +1510,7 @@ namespace Emby.Server.Implementations return GetLocalApiUrl(ipAddress.ToString()); } - /// + /// public string GetLocalApiUrl(ReadOnlySpan host) { var url = new StringBuilder(64); @@ -1559,7 +1559,7 @@ namespace Emby.Server.Implementations } } - var valid = await IsIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false); + var valid = await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false); if (valid) { resultList.Add(address); @@ -1593,7 +1593,7 @@ namespace Emby.Server.Implementations private readonly ConcurrentDictionary _validAddressResults = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - private async Task IsIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken) + private async Task IsLocalIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken) { if (address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback)) diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index d999f76db..8537e4180 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -56,29 +56,33 @@ namespace MediaBrowser.Controller string FriendlyName { get; } /// - /// Gets the local ip address. + /// Gets all the local IP addresses of this API instance. Each address is validated by sending a 'ping' request + /// to the API that should exist at the address. /// - /// The local ip address. + /// A cancellation token that can be used to cancel the task. + /// A list containing all the local IP addresses of the server. Task> GetLocalIpAddresses(CancellationToken cancellationToken); /// - /// Gets the local API URL. + /// Gets a local (LAN) URL that can be used to access the API. The hostname used is the first valid configured + /// IP address that can be found via . /// - /// The local API URL. + /// A cancellation token that can be used to cancel the task. + /// The server URL. Task GetLocalApiUrl(CancellationToken cancellationToken); /// - /// Gets the local API URL. + /// Gets a local (LAN) URL that can be used to access the API. /// - /// The hostname. - /// The local API URL. + /// The hostname to use in the URL. + /// The API URL. string GetLocalApiUrl(ReadOnlySpan hostname); /// - /// Gets the local API URL. + /// Gets a local (LAN) URL that can be used to access the API. /// - /// The IP address. - /// The local API URL. + /// The IP address to use as the hostname in the URL. + /// The API URL. string GetLocalApiUrl(IPAddress address); /// -- cgit v1.2.3 From 57b5ec1d514ad8c5a0e8aced4b84b46b4c8923a7 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sun, 26 Apr 2020 12:07:54 -0400 Subject: Remove unnecessary properties from SystemInfo response object These properties do not provide any useful information to the client. The client would already have to have all this information in order to connect to the endpoint to retrieve it --- Emby.Server.Implementations/ApplicationHost.cs | 4 ---- Jellyfin.Server/Program.cs | 3 --- .../Configuration/ServerConfiguration.cs | 5 ----- MediaBrowser.Model/System/SystemInfo.cs | 18 ------------------ 4 files changed, 30 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 9655d9f5e..edfed67a1 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -94,7 +94,6 @@ using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using MediaBrowser.Model.System; using MediaBrowser.Model.Tasks; -using MediaBrowser.Model.Updates; using MediaBrowser.Providers.Chapters; using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.TheTvdb; @@ -1143,9 +1142,6 @@ namespace Emby.Server.Implementations ItemsByNamePath = ApplicationPaths.InternalMetadataPath, InternalMetadataPath = ApplicationPaths.InternalMetadataPath, CachePath = ApplicationPaths.CachePath, - HttpServerPortNumber = HttpPort, - SupportsHttps = ListenWithHttps || ServerConfigurationManager.Configuration.IsBehindProxy, - HttpsPortNumber = HttpsPort, OperatingSystem = OperatingSystem.Id.ToString(), OperatingSystemDisplayName = OperatingSystem.Name, CanSelfRestart = CanSelfRestart, diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 7aa238efa..1c586ffdd 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -10,14 +10,11 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using CommandLine; -using Emby.Drawing; using Emby.Server.Implementations; using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Networking; -using Jellyfin.Drawing.Skia; using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Extensions; using MediaBrowser.WebDashboard.Api; using Microsoft.AspNetCore.Hosting; diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 6ee6a1f93..e4cd6c34a 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -234,11 +234,6 @@ namespace MediaBrowser.Model.Configuration /// public bool RequireHttps { get; set; } - /// - /// Gets or sets a value indicating whether the server is behind a reverse proxy. - /// - public bool IsBehindProxy { get; set; } - public bool EnableNewOmdbSupport { get; set; } public string[] RemoteIPFilter { get; set; } diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index 9753f4e06..f2c5aa1e3 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -115,24 +115,6 @@ namespace MediaBrowser.Model.System /// The transcode path. public string TranscodingTempPath { get; set; } - /// - /// Gets or sets the HTTP server port number. - /// - /// The HTTP server port number. - public int HttpServerPortNumber { get; set; } - - /// - /// Gets or sets a value indicating whether a client can connect to the server over HTTPS, either directly or - /// via a reverse proxy. - /// - public bool SupportsHttps { get; set; } - - /// - /// Gets or sets the HTTPS server port number. - /// - /// The HTTPS server port number. - public int HttpsPortNumber { get; set; } - /// /// Gets or sets a value indicating whether this instance has update available. /// -- cgit v1.2.3 From 15fd4812f09282e9b81f53845ce462f42ff1b5e9 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sun, 26 Apr 2020 18:04:34 -0400 Subject: Remove unnecessary foreach loop --- Emby.Server.Implementations/ApplicationHost.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index edfed67a1..8f20a4921 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1187,13 +1187,12 @@ namespace Emby.Server.Implementations { // Return the first matched address, if found, or the first known local address var addresses = await GetLocalIpAddressesInternal(false, 1, cancellationToken).ConfigureAwait(false); - - foreach (var address in addresses) + if (addresses.Count == 0) { - return GetLocalApiUrl(address); + return null; } - return null; + return GetLocalApiUrl(addresses.First()); } catch (Exception ex) { -- cgit v1.2.3 From 10c2c62f07fb4088480ff580ab67c1bc10a057a4 Mon Sep 17 00:00:00 2001 From: gion Date: Wed, 1 Apr 2020 17:52:42 +0200 Subject: Implement syncplay backend --- Emby.Server.Implementations/ApplicationHost.cs | 4 + .../Session/SessionManager.cs | 17 + .../Syncplay/SyncplayController.cs | 418 +++++++++++++++++++++ .../Syncplay/SyncplayManager.cs | 248 ++++++++++++ MediaBrowser.Api/Syncplay/SyncplayService.cs | 261 +++++++++++++ MediaBrowser.Controller/Session/ISessionManager.cs | 19 + MediaBrowser.Controller/Syncplay/GroupInfo.cs | 148 ++++++++ MediaBrowser.Controller/Syncplay/GroupMember.cs | 28 ++ .../Syncplay/ISyncplayController.cs | 61 +++ .../Syncplay/ISyncplayManager.cs | 62 +++ MediaBrowser.Model/Syncplay/GroupInfoModel.cs | 38 ++ MediaBrowser.Model/Syncplay/SyncplayCommand.cs | 32 ++ MediaBrowser.Model/Syncplay/SyncplayCommandType.cs | 21 ++ MediaBrowser.Model/Syncplay/SyncplayGroupUpdate.cs | 26 ++ .../Syncplay/SyncplayGroupUpdateType.cs | 41 ++ MediaBrowser.Model/Syncplay/SyncplayRequestInfo.cs | 34 ++ MediaBrowser.Model/Syncplay/SyncplayRequestType.cs | 33 ++ 17 files changed, 1491 insertions(+) create mode 100644 Emby.Server.Implementations/Syncplay/SyncplayController.cs create mode 100644 Emby.Server.Implementations/Syncplay/SyncplayManager.cs create mode 100644 MediaBrowser.Api/Syncplay/SyncplayService.cs create mode 100644 MediaBrowser.Controller/Syncplay/GroupInfo.cs create mode 100644 MediaBrowser.Controller/Syncplay/GroupMember.cs create mode 100644 MediaBrowser.Controller/Syncplay/ISyncplayController.cs create mode 100644 MediaBrowser.Controller/Syncplay/ISyncplayManager.cs create mode 100644 MediaBrowser.Model/Syncplay/GroupInfoModel.cs create mode 100644 MediaBrowser.Model/Syncplay/SyncplayCommand.cs create mode 100644 MediaBrowser.Model/Syncplay/SyncplayCommandType.cs create mode 100644 MediaBrowser.Model/Syncplay/SyncplayGroupUpdate.cs create mode 100644 MediaBrowser.Model/Syncplay/SyncplayGroupUpdateType.cs create mode 100644 MediaBrowser.Model/Syncplay/SyncplayRequestInfo.cs create mode 100644 MediaBrowser.Model/Syncplay/SyncplayRequestType.cs (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 4d906a1bf..8419014c2 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -47,6 +47,7 @@ using Emby.Server.Implementations.Session; using Emby.Server.Implementations.SocketSharp; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; +using Emby.Server.Implementations.Syncplay; using MediaBrowser.Api; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; @@ -80,6 +81,7 @@ using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Controller.TV; +using MediaBrowser.Controller.Syncplay; using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.Model.Activity; @@ -643,6 +645,8 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index c93c02c48..b1519b572 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -25,6 +25,7 @@ using MediaBrowser.Model.Events; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; +using MediaBrowser.Model.Syncplay; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Session @@ -1154,6 +1155,22 @@ namespace Emby.Server.Implementations.Session await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false); } + /// + public async Task SendSyncplayCommand(string sessionId, SyncplayCommand command, CancellationToken cancellationToken) + { + CheckDisposed(); + var session = GetSessionToRemoteControl(sessionId); + await SendMessageToSession(session, "SyncplayCommand", command, cancellationToken).ConfigureAwait(false); + } + + /// + public async Task SendSyncplayGroupUpdate(string sessionId, SyncplayGroupUpdate command, CancellationToken cancellationToken) + { + CheckDisposed(); + var session = GetSessionToRemoteControl(sessionId); + await SendMessageToSession(session, "SyncplayGroupUpdate", command, cancellationToken).ConfigureAwait(false); + } + private IEnumerable TranslateItemForPlayback(Guid id, User user) { var item = _libraryManager.GetItemById(id); diff --git a/Emby.Server.Implementations/Syncplay/SyncplayController.cs b/Emby.Server.Implementations/Syncplay/SyncplayController.cs new file mode 100644 index 000000000..4a20ceba0 --- /dev/null +++ b/Emby.Server.Implementations/Syncplay/SyncplayController.cs @@ -0,0 +1,418 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.Syncplay; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.Syncplay; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.Syncplay +{ + /// + /// Class SyncplayController. + /// + public class SyncplayController : ISyncplayController, IDisposable + { + private enum BroadcastType + { + AllGroup = 0, + SingleUser = 1, + AllExceptUser = 2, + AllReady = 3 + } + + /// + /// The logger. + /// + private readonly ILogger _logger; + + /// + /// The session manager. + /// + private readonly ISessionManager _sessionManager; + + /// + /// The syncplay manager. + /// + private readonly ISyncplayManager _syncplayManager; + + /// + /// The group to manage. + /// + private readonly GroupInfo _group = new GroupInfo(); + + /// + public Guid GetGroupId() => _group.GroupId; + + /// + public Guid GetPlayingItemId() => _group.PlayingItem.Id; + + /// + public bool IsGroupEmpty() => _group.IsEmpty(); + + private bool _disposed = false; + + public SyncplayController( + ILogger logger, + ISessionManager sessionManager, + ISyncplayManager syncplayManager) + { + _logger = logger; + _sessionManager = sessionManager; + _syncplayManager = syncplayManager; + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and optionally managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + _disposed = true; + } + + // TODO: use this somewhere + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + + private SessionInfo[] FilterUsers(SessionInfo from, BroadcastType type) + { + if (type == BroadcastType.SingleUser) + { + return new SessionInfo[] { from }; + } + else if (type == BroadcastType.AllGroup) + { + return _group.Partecipants.Values.Select( + user => user.Session + ).ToArray(); + } + else if (type == BroadcastType.AllExceptUser) + { + return _group.Partecipants.Values.Select( + user => user.Session + ).Where( + user => !user.Id.Equals(from.Id) + ).ToArray(); + } + else if (type == BroadcastType.AllReady) + { + return _group.Partecipants.Values.Where( + user => !user.IsBuffering + ).Select( + user => user.Session + ).ToArray(); + } + else + { + return new SessionInfo[] {}; + } + } + + private Task SendGroupUpdate(SessionInfo from, BroadcastType type, SyncplayGroupUpdate message) + { + IEnumerable GetTasks() + { + SessionInfo[] users = FilterUsers(from, type); + foreach (var user in users) + { + yield return _sessionManager.SendSyncplayGroupUpdate(user.Id.ToString(), message, CancellationToken.None); + } + } + + return Task.WhenAll(GetTasks()); + } + + private Task SendCommand(SessionInfo from, BroadcastType type, SyncplayCommand message) + { + IEnumerable GetTasks() + { + SessionInfo[] users = FilterUsers(from, type); + foreach (var user in users) + { + yield return _sessionManager.SendSyncplayCommand(user.Id.ToString(), message, CancellationToken.None); + } + } + + return Task.WhenAll(GetTasks()); + } + + private SyncplayCommand NewSyncplayCommand(SyncplayCommandType type) { + var command = new SyncplayCommand(); + command.GroupId = _group.GroupId.ToString(); + command.Command = type; + command.PositionTicks = _group.PositionTicks; + command.When = _group.LastActivity.ToUniversalTime().ToString("o"); + return command; + } + + private SyncplayGroupUpdate NewSyncplayGroupUpdate(SyncplayGroupUpdateType type, T data) + { + var command = new SyncplayGroupUpdate(); + command.GroupId = _group.GroupId.ToString(); + command.Type = type; + command.Data = data; + return command; + } + + /// + public void InitGroup(SessionInfo user) + { + _group.AddUser(user); + _syncplayManager.MapUserToGroup(user, this); + + _group.PlayingItem = user.FullNowPlayingItem; + _group.IsPaused = true; + _group.PositionTicks = user.PlayState.PositionTicks ??= 0; + _group.LastActivity = DateTime.UtcNow; + + var updateUser = NewSyncplayGroupUpdate(SyncplayGroupUpdateType.GroupJoined, DateTime.UtcNow.ToUniversalTime().ToString("o")); + SendGroupUpdate(user, BroadcastType.SingleUser, updateUser); + var pauseCommand = NewSyncplayCommand(SyncplayCommandType.Pause); + SendCommand(user, BroadcastType.SingleUser, pauseCommand); + } + + /// + public void UserJoin(SessionInfo user) + { + if (user.NowPlayingItem != null && user.NowPlayingItem.Id.Equals(_group.PlayingItem.Id)) + { + _group.AddUser(user); + _syncplayManager.MapUserToGroup(user, this); + + var updateUser = NewSyncplayGroupUpdate(SyncplayGroupUpdateType.GroupJoined, _group.PositionTicks); + SendGroupUpdate(user, BroadcastType.SingleUser, updateUser); + + var updateOthers = NewSyncplayGroupUpdate(SyncplayGroupUpdateType.UserJoined, user.UserName); + SendGroupUpdate(user, BroadcastType.AllExceptUser, updateOthers); + + // Client join and play, syncing will happen client side + if (!_group.IsPaused) + { + var playCommand = NewSyncplayCommand(SyncplayCommandType.Play); + SendCommand(user, BroadcastType.SingleUser, playCommand); + } + else + { + var pauseCommand = NewSyncplayCommand(SyncplayCommandType.Pause); + SendCommand(user, BroadcastType.SingleUser, pauseCommand); + } + } + else + { + var playRequest = new PlayRequest(); + playRequest.ItemIds = new Guid[] { _group.PlayingItem.Id }; + playRequest.StartPositionTicks = _group.PositionTicks; + var update = NewSyncplayGroupUpdate(SyncplayGroupUpdateType.PrepareSession, playRequest); + SendGroupUpdate(user, BroadcastType.SingleUser, update); + } + } + + /// + public void UserLeave(SessionInfo user) + { + _group.RemoveUser(user); + _syncplayManager.UnmapUserFromGroup(user, this); + + var updateUser = NewSyncplayGroupUpdate(SyncplayGroupUpdateType.GroupLeft, _group.PositionTicks); + SendGroupUpdate(user, BroadcastType.SingleUser, updateUser); + + var updateOthers = NewSyncplayGroupUpdate(SyncplayGroupUpdateType.UserLeft, user.UserName); + SendGroupUpdate(user, BroadcastType.AllExceptUser, updateOthers); + } + + /// + public void HandleRequest(SessionInfo user, SyncplayRequestInfo request) + { + if (request.Type.Equals(SyncplayRequestType.Play)) + { + if (_group.IsPaused) + { + var delay = _group.GetHighestPing() * 2; + delay = delay < _group.DefaulPing ? _group.DefaulPing : delay; + + _group.IsPaused = false; + _group.LastActivity = DateTime.UtcNow.AddMilliseconds( + delay + ); + + var command = NewSyncplayCommand(SyncplayCommandType.Play); + SendCommand(user, BroadcastType.AllGroup, command); + } + else + { + // Client got lost + var command = NewSyncplayCommand(SyncplayCommandType.Play); + SendCommand(user, BroadcastType.SingleUser, command); + } + } + else if (request.Type.Equals(SyncplayRequestType.Pause)) + { + if (!_group.IsPaused) + { + _group.IsPaused = true; + var currentTime = DateTime.UtcNow; + var elapsedTime = currentTime - _group.LastActivity; + _group.LastActivity = currentTime; + _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0; + + var command = NewSyncplayCommand(SyncplayCommandType.Pause); + SendCommand(user, BroadcastType.AllGroup, command); + } + else + { + var command = NewSyncplayCommand(SyncplayCommandType.Pause); + SendCommand(user, BroadcastType.SingleUser, command); + } + } + else if (request.Type.Equals(SyncplayRequestType.Seek)) + { + // Sanitize PositionTicks + var ticks = request.PositionTicks ??= 0; + ticks = ticks >= 0 ? ticks : 0; + if (_group.PlayingItem.RunTimeTicks != null) + { + var runTimeTicks = _group.PlayingItem.RunTimeTicks ??= 0; + ticks = ticks > runTimeTicks ? runTimeTicks : ticks; + } + + _group.IsPaused = true; + _group.PositionTicks = ticks; + _group.LastActivity = DateTime.UtcNow; + + var command = NewSyncplayCommand(SyncplayCommandType.Seek); + SendCommand(user, BroadcastType.AllGroup, command); + } + // TODO: client does not implement this yet + else if (request.Type.Equals(SyncplayRequestType.Buffering)) + { + if (!_group.IsPaused) + { + _group.IsPaused = true; + var currentTime = DateTime.UtcNow; + var elapsedTime = currentTime - _group.LastActivity; + _group.LastActivity = currentTime; + _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0; + + _group.SetBuffering(user, true); + + // Send pause command to all non-buffering users + var command = NewSyncplayCommand(SyncplayCommandType.Pause); + SendCommand(user, BroadcastType.AllReady, command); + + var updateOthers = NewSyncplayGroupUpdate(SyncplayGroupUpdateType.GroupWait, user.UserName); + SendGroupUpdate(user, BroadcastType.AllExceptUser, updateOthers); + } + else + { + var command = NewSyncplayCommand(SyncplayCommandType.Pause); + SendCommand(user, BroadcastType.SingleUser, command); + } + } + // TODO: client does not implement this yet + else if (request.Type.Equals(SyncplayRequestType.BufferingComplete)) + { + if (_group.IsPaused) + { + _group.SetBuffering(user, false); + + if (_group.IsBuffering()) { + // Others are buffering, tell this client to pause when ready + var when = request.When ??= DateTime.UtcNow; + var currentTime = DateTime.UtcNow; + var elapsedTime = currentTime - when; + var clientPosition = TimeSpan.FromTicks(request.PositionTicks ??= 0) + elapsedTime; + var delay = _group.PositionTicks - clientPosition.Ticks; + + var command = NewSyncplayCommand(SyncplayCommandType.Pause); + command.When = currentTime.AddMilliseconds( + delay + ).ToUniversalTime().ToString("o"); + SendCommand(user, BroadcastType.SingleUser, command); + } + else + { + // Let other clients resume as soon as the buffering client catches up + var when = request.When ??= DateTime.UtcNow; + var currentTime = DateTime.UtcNow; + var elapsedTime = currentTime - when; + var clientPosition = TimeSpan.FromTicks(request.PositionTicks ??= 0) + elapsedTime; + var delay = _group.PositionTicks - clientPosition.Ticks; + + _group.IsPaused = false; + + if (delay > _group.GetHighestPing() * 2) + { + // Client that was buffering is recovering, notifying others to resume + _group.LastActivity = currentTime.AddMilliseconds( + delay + ); + var command = NewSyncplayCommand(SyncplayCommandType.Play); + SendCommand(user, BroadcastType.AllExceptUser, command); + } + else + { + // Client, that was buffering, resumed playback but did not update others in time + delay = _group.GetHighestPing() * 2; + delay = delay < _group.DefaulPing ? _group.DefaulPing : delay; + + _group.LastActivity = currentTime.AddMilliseconds( + delay + ); + + var command = NewSyncplayCommand(SyncplayCommandType.Play); + SendCommand(user, BroadcastType.AllGroup, command); + } + } + } + else + { + // Make sure client has latest group state + var command = NewSyncplayCommand(SyncplayCommandType.Play); + SendCommand(user, BroadcastType.SingleUser, command); + } + } + else if (request.Type.Equals(SyncplayRequestType.KeepAlive)) + { + _group.UpdatePing(user, request.Ping ??= _group.DefaulPing); + + var keepAlive = new SyncplayGroupUpdate(); + keepAlive.GroupId = _group.GroupId.ToString(); + keepAlive.Type = SyncplayGroupUpdateType.KeepAlive; + SendGroupUpdate(user, BroadcastType.SingleUser, keepAlive); + } + } + + /// + public GroupInfoView GetInfo() + { + var info = new GroupInfoView(); + info.GroupId = GetGroupId().ToString(); + info.PlayingItemName = _group.PlayingItem.Name; + info.PlayingItemId = _group.PlayingItem.Id.ToString(); + info.PositionTicks = _group.PositionTicks; + info.Partecipants = _group.Partecipants.Values.Select(user => user.Session.UserName).ToArray(); + return info; + } + } +} diff --git a/Emby.Server.Implementations/Syncplay/SyncplayManager.cs b/Emby.Server.Implementations/Syncplay/SyncplayManager.cs new file mode 100644 index 000000000..6bfd6aa9b --- /dev/null +++ b/Emby.Server.Implementations/Syncplay/SyncplayManager.cs @@ -0,0 +1,248 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Microsoft.Extensions.Logging; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.Syncplay; +using MediaBrowser.Model.Syncplay; + +namespace Emby.Server.Implementations.Syncplay +{ + /// + /// Class SyncplayManager. + /// + public class SyncplayManager : ISyncplayManager, IDisposable + { + /// + /// The logger. + /// + private readonly ILogger _logger; + + /// + /// The session manager. + /// + private readonly ISessionManager _sessionManager; + + /// + /// The map between users and groups. + /// + private readonly ConcurrentDictionary _userToGroupMap = + new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// The groups. + /// + private readonly ConcurrentDictionary _groups = + new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + private bool _disposed = false; + + public SyncplayManager( + ILogger logger, + ISessionManager sessionManager) + { + _logger = logger; + _sessionManager = sessionManager; + + _sessionManager.SessionEnded += _sessionManager_SessionEnded; + _sessionManager.PlaybackStopped += _sessionManager_PlaybackStopped; + } + + /// + /// Gets all groups. + /// + /// All groups. + public IEnumerable Groups => _groups.Values; + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and optionally managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + _sessionManager.SessionEnded -= _sessionManager_SessionEnded; + _sessionManager.PlaybackStopped -= _sessionManager_PlaybackStopped; + + _disposed = true; + } + + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + + void _sessionManager_SessionEnded(object sender, SessionEventArgs e) + { + LeaveGroup(e.SessionInfo); + } + + void _sessionManager_PlaybackStopped(object sender, PlaybackStopEventArgs e) + { + LeaveGroup(e.Session); + } + + private bool IsUserInGroup(SessionInfo user) + { + return _userToGroupMap.ContainsKey(user.Id); + } + + private Guid? GetUserGroup(SessionInfo user) + { + ISyncplayController group; + _userToGroupMap.TryGetValue(user.Id, out group); + if (group != null) + { + return group.GetGroupId(); + } + else + { + return null; + } + } + + /// + public void NewGroup(SessionInfo user) + { + if (IsUserInGroup(user)) + { + LeaveGroup(user); + } + + var group = new SyncplayController(_logger, _sessionManager, this); + _groups[group.GetGroupId().ToString()] = group; + + group.InitGroup(user); + } + + /// + public void JoinGroup(SessionInfo user, string groupId) + { + if (IsUserInGroup(user)) + { + if (GetUserGroup(user).Equals(groupId)) return; + LeaveGroup(user); + } + + ISyncplayController group; + _groups.TryGetValue(groupId, out group); + + if (group == null) + { + _logger.LogError("Syncplaymanager JoinGroup: " + groupId + " does not exist."); + + var update = new SyncplayGroupUpdate(); + update.Type = SyncplayGroupUpdateType.NotInGroup; + _sessionManager.SendSyncplayGroupUpdate(user.Id.ToString(), update, CancellationToken.None); + return; + } + group.UserJoin(user); + } + + /// + public void LeaveGroup(SessionInfo user) + { + ISyncplayController group; + _userToGroupMap.TryGetValue(user.Id, out group); + + if (group == null) + { + _logger.LogWarning("Syncplaymanager HandleRequest: " + user.Id + " not in group."); + + var update = new SyncplayGroupUpdate(); + update.Type = SyncplayGroupUpdateType.NotInGroup; + _sessionManager.SendSyncplayGroupUpdate(user.Id.ToString(), update, CancellationToken.None); + return; + } + group.UserLeave(user); + + if (group.IsGroupEmpty()) + { + _groups.Remove(group.GetGroupId().ToString(), out _); + } + } + + /// + public List ListGroups(SessionInfo user) + { + // Filter by playing item if the user is viewing something already + if (user.NowPlayingItem != null) + { + return _groups.Values.Where( + group => group.GetPlayingItemId().Equals(user.FullNowPlayingItem.Id) + ).Select( + group => group.GetInfo() + ).ToList(); + } + // Otherwise show all available groups + else + { + return _groups.Values.Select( + group => group.GetInfo() + ).ToList(); + } + } + + /// + public void HandleRequest(SessionInfo user, SyncplayRequestInfo request) + { + ISyncplayController group; + _userToGroupMap.TryGetValue(user.Id, out group); + + if (group == null) + { + _logger.LogWarning("Syncplaymanager HandleRequest: " + user.Id + " not in group."); + + var update = new SyncplayGroupUpdate(); + update.Type = SyncplayGroupUpdateType.NotInGroup; + _sessionManager.SendSyncplayGroupUpdate(user.Id.ToString(), update, CancellationToken.None); + return; + } + group.HandleRequest(user, request); + } + + /// + public void MapUserToGroup(SessionInfo user, ISyncplayController group) + { + if (IsUserInGroup(user)) + { + throw new InvalidOperationException("User in other group already!"); + } + _userToGroupMap[user.Id] = group; + } + + /// + public void UnmapUserFromGroup(SessionInfo user, ISyncplayController group) + { + if (!IsUserInGroup(user)) + { + throw new InvalidOperationException("User not in any group!"); + } + + ISyncplayController tempGroup; + _userToGroupMap.Remove(user.Id, out tempGroup); + + if (!tempGroup.GetGroupId().Equals(group.GetGroupId())) + { + throw new InvalidOperationException("User was in wrong group!"); + } + } + } +} diff --git a/MediaBrowser.Api/Syncplay/SyncplayService.cs b/MediaBrowser.Api/Syncplay/SyncplayService.cs new file mode 100644 index 000000000..f17cca9ee --- /dev/null +++ b/MediaBrowser.Api/Syncplay/SyncplayService.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.Syncplay; +using MediaBrowser.Model.Services; +using MediaBrowser.Model.Syncplay; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Api.Syncplay +{ + [Route("/Syncplay/{SessionId}/NewGroup", "POST", Summary = "Create a new Syncplay group")] + [Authenticated] + public class SyncplayNewGroup : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + } + + [Route("/Syncplay/{SessionId}/JoinGroup", "POST", Summary = "Join an existing Syncplay group")] + [Authenticated] + public class SyncplayJoinGroup : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + + /// + /// Gets or sets the Group id. + /// + /// The Group id to join. + [ApiMember(Name = "GroupId", Description = "Group Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string GroupId { get; set; } + } + + [Route("/Syncplay/{SessionId}/LeaveGroup", "POST", Summary = "Leave joined Syncplay group")] + [Authenticated] + public class SyncplayLeaveGroup : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + } + + [Route("/Syncplay/{SessionId}/ListGroups", "POST", Summary = "List Syncplay groups playing same item")] + [Authenticated] + public class SyncplayListGroups : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + } + + [Route("/Syncplay/{SessionId}/PlayRequest", "POST", Summary = "Request play in Syncplay group")] + [Authenticated] + public class SyncplayPlayRequest : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + } + + [Route("/Syncplay/{SessionId}/PauseRequest", "POST", Summary = "Request pause in Syncplay group")] + [Authenticated] + public class SyncplayPauseRequest : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + } + + [Route("/Syncplay/{SessionId}/SeekRequest", "POST", Summary = "Request seek in Syncplay group")] + [Authenticated] + public class SyncplaySeekRequest : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + + [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")] + public long PositionTicks { get; set; } + } + + [Route("/Syncplay/{SessionId}/BufferingRequest", "POST", Summary = "Request group wait in Syncplay group while buffering")] + [Authenticated] + public class SyncplayBufferingRequest : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + + [ApiMember(Name = "When", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string When { get; set; } + + [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")] + public long PositionTicks { get; set; } + + [ApiMember(Name = "Resume", IsRequired = true, DataType = "bool", ParameterType = "query", Verb = "POST")] + public bool Resume { get; set; } + } + + [Route("/Syncplay/{SessionId}/KeepAlive", "POST", Summary = "Keep session alive")] + [Authenticated] + public class SyncplayKeepAlive : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + + [ApiMember(Name = "Ping", IsRequired = true, DataType = "double", ParameterType = "query", Verb = "POST")] + public double Ping { get; set; } + } + + [Route("/Syncplay/{SessionId}/GetUtcTime", "POST", Summary = "Get UtcTime")] + [Authenticated] + public class SyncplayGetUtcTime : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + } + + /// + /// Class SyncplayService. + /// + public class SyncplayService : BaseApiService + { + /// + /// The session manager. + /// + private readonly ISessionManager _sessionManager; + + private readonly ISessionContext _sessionContext; + + /// + /// The Syncplay manager. + /// + private readonly ISyncplayManager _syncplayManager; + + public SyncplayService( + ILogger logger, + IServerConfigurationManager serverConfigurationManager, + IHttpResultFactory httpResultFactory, + ISessionManager sessionManager, + ISessionContext sessionContext, + ISyncplayManager syncplayManager) + : base(logger, serverConfigurationManager, httpResultFactory) + { + _sessionManager = sessionManager; + _sessionContext = sessionContext; + _syncplayManager = syncplayManager; + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncplayNewGroup request) + { + var currentSession = GetSession(_sessionContext); + _syncplayManager.NewGroup(currentSession); + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncplayJoinGroup request) + { + var currentSession = GetSession(_sessionContext); + _syncplayManager.JoinGroup(currentSession, request.GroupId); + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncplayLeaveGroup request) + { + var currentSession = GetSession(_sessionContext); + _syncplayManager.LeaveGroup(currentSession); + } + + /// + /// Handles the specified request. + /// + /// The request. + /// The requested list of groups. + public List Post(SyncplayListGroups request) + { + var currentSession = GetSession(_sessionContext); + return _syncplayManager.ListGroups(currentSession); + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncplayPlayRequest request) + { + var currentSession = GetSession(_sessionContext); + var syncplayRequest = new SyncplayRequestInfo(); + syncplayRequest.Type = SyncplayRequestType.Play; + _syncplayManager.HandleRequest(currentSession, syncplayRequest); + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncplayPauseRequest request) + { + var currentSession = GetSession(_sessionContext); + var syncplayRequest = new SyncplayRequestInfo(); + syncplayRequest.Type = SyncplayRequestType.Pause; + _syncplayManager.HandleRequest(currentSession, syncplayRequest); + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncplaySeekRequest request) + { + var currentSession = GetSession(_sessionContext); + var syncplayRequest = new SyncplayRequestInfo(); + syncplayRequest.Type = SyncplayRequestType.Seek; + syncplayRequest.PositionTicks = request.PositionTicks; + _syncplayManager.HandleRequest(currentSession, syncplayRequest); + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncplayBufferingRequest request) + { + var currentSession = GetSession(_sessionContext); + var syncplayRequest = new SyncplayRequestInfo(); + syncplayRequest.Type = request.Resume ? SyncplayRequestType.BufferingComplete : SyncplayRequestType.Buffering; + syncplayRequest.When = DateTime.Parse(request.When); + syncplayRequest.PositionTicks = request.PositionTicks; + _syncplayManager.HandleRequest(currentSession, syncplayRequest); + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncplayKeepAlive request) + { + var currentSession = GetSession(_sessionContext); + var syncplayRequest = new SyncplayRequestInfo(); + syncplayRequest.Type = SyncplayRequestType.KeepAlive; + syncplayRequest.Ping = Convert.ToInt64(request.Ping); + _syncplayManager.HandleRequest(currentSession, syncplayRequest); + } + + /// + /// Handles the specified request. + /// + /// The request. + /// The current UTC time. + public string Post(SyncplayGetUtcTime request) + { + return DateTime.UtcNow.ToUniversalTime().ToString("o"); + } + } +} diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 771027103..4bfc0c73f 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -9,6 +9,7 @@ using MediaBrowser.Controller.Security; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Events; using MediaBrowser.Model.Session; +using MediaBrowser.Model.Syncplay; namespace MediaBrowser.Controller.Session { @@ -140,6 +141,24 @@ namespace MediaBrowser.Controller.Session /// Task. Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken); + /// + /// Sends the SyncplayCommand. + /// + /// The session id. + /// The command. + /// The cancellation token. + /// Task. + Task SendSyncplayCommand(string sessionId, SyncplayCommand command, CancellationToken cancellationToken); + + /// + /// Sends the SyncplayGroupUpdate. + /// + /// The session id. + /// The group update. + /// The cancellation token. + /// Task. + Task SendSyncplayGroupUpdate(string sessionId, SyncplayGroupUpdate command, CancellationToken cancellationToken); + /// /// Sends the browse command. /// diff --git a/MediaBrowser.Controller/Syncplay/GroupInfo.cs b/MediaBrowser.Controller/Syncplay/GroupInfo.cs new file mode 100644 index 000000000..d37e8563b --- /dev/null +++ b/MediaBrowser.Controller/Syncplay/GroupInfo.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.Syncplay +{ + /// + /// Class GroupInfo. + /// + public class GroupInfo + { + /// + /// Default ping value used for users. + /// + public readonly long DefaulPing = 500; + /// + /// Gets or sets the group identifier. + /// + /// The group identifier. + public readonly Guid GroupId = Guid.NewGuid(); + + /// + /// Gets or sets the playing item. + /// + /// The playing item. + public BaseItem PlayingItem { get; set; } + + /// + /// Gets or sets whether playback is paused. + /// + /// Playback is paused. + public bool IsPaused { get; set; } + + /// + /// Gets or sets the position ticks. + /// + /// The position ticks. + public long PositionTicks { get; set; } + + /// + /// Gets or sets the last activity. + /// + /// The last activity. + public DateTime LastActivity { get; set; } + + /// + /// Gets the partecipants. + /// + /// The partecipants. + public readonly ConcurrentDictionary Partecipants = + new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Checks if a user is in this group. + /// + /// true if the user is in this group; false otherwise. + public bool ContainsUser(string sessionId) + { + return Partecipants.ContainsKey(sessionId); + } + + /// + /// Adds the user to the group. + /// + /// The session. + public void AddUser(SessionInfo user) + { + if (ContainsUser(user.Id.ToString())) return; + var member = new GroupMember(); + member.Session = user; + member.Ping = DefaulPing; + member.IsBuffering = false; + Partecipants[user.Id.ToString()] = member; + } + + /// + /// Removes the user from the group. + /// + /// The session. + + public void RemoveUser(SessionInfo user) + { + if (!ContainsUser(user.Id.ToString())) return; + GroupMember member; + Partecipants.Remove(user.Id.ToString(), out member); + } + + /// + /// Updates the ping of a user. + /// + /// The session. + /// The ping. + public void UpdatePing(SessionInfo user, long ping) + { + if (!ContainsUser(user.Id.ToString())) return; + Partecipants[user.Id.ToString()].Ping = ping; + } + + /// + /// Gets the highest ping in the group. + /// + /// The highest ping in the group. + public long GetHighestPing() + { + long max = Int64.MinValue; + foreach (var user in Partecipants.Values) + { + max = Math.Max(max, user.Ping); + } + return max; + } + + /// + /// Sets the user's buffering state. + /// + /// The session. + /// The state. + public void SetBuffering(SessionInfo user, bool isBuffering) + { + if (!ContainsUser(user.Id.ToString())) return; + Partecipants[user.Id.ToString()].IsBuffering = isBuffering; + } + + /// + /// Gets the group buffering state. + /// + /// true if there is a user buffering in the group; false otherwise. + public bool IsBuffering() + { + foreach (var user in Partecipants.Values) + { + if (user.IsBuffering) return true; + } + return false; + } + + /// + /// Checks if the group is empty. + /// + /// true if the group is empty; false otherwise. + public bool IsEmpty() + { + return Partecipants.Count == 0; + } + } +} diff --git a/MediaBrowser.Controller/Syncplay/GroupMember.cs b/MediaBrowser.Controller/Syncplay/GroupMember.cs new file mode 100644 index 000000000..7630428d7 --- /dev/null +++ b/MediaBrowser.Controller/Syncplay/GroupMember.cs @@ -0,0 +1,28 @@ +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.Syncplay +{ + /// + /// Class GroupMember. + /// + public class GroupMember + { + /// + /// Gets or sets whether this member is buffering. + /// + /// true if member is buffering; false otherwise. + public bool IsBuffering { get; set; } + + /// + /// Gets or sets the session. + /// + /// The session. + public SessionInfo Session { get; set; } + + /// + /// Gets or sets the ping. + /// + /// The ping. + public long Ping { get; set; } + } +} diff --git a/MediaBrowser.Controller/Syncplay/ISyncplayController.cs b/MediaBrowser.Controller/Syncplay/ISyncplayController.cs new file mode 100644 index 000000000..c9465b27a --- /dev/null +++ b/MediaBrowser.Controller/Syncplay/ISyncplayController.cs @@ -0,0 +1,61 @@ +using System; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Syncplay; + +namespace MediaBrowser.Controller.Syncplay +{ + /// + /// Interface ISyncplayController. + /// + public interface ISyncplayController + { + /// + /// Gets the group id. + /// + /// The group id. + Guid GetGroupId(); + + /// + /// Gets the playing item id. + /// + /// The playing item id. + Guid GetPlayingItemId(); + + /// + /// Checks if the group is empty. + /// + /// If the group is empty. + bool IsGroupEmpty(); + + /// + /// Initializes the group with the user's info. + /// + /// The session. + void InitGroup(SessionInfo user); + + /// + /// Adds the user to the group. + /// + /// The session. + void UserJoin(SessionInfo user); + + /// + /// Removes the user from the group. + /// + /// The session. + void UserLeave(SessionInfo user); + + /// + /// Handles the requested action by the user. + /// + /// The session. + /// The requested action. + void HandleRequest(SessionInfo user, SyncplayRequestInfo request); + + /// + /// Gets the info about the group for the clients. + /// + /// The group info for the clients. + GroupInfoView GetInfo(); + } +} \ No newline at end of file diff --git a/MediaBrowser.Controller/Syncplay/ISyncplayManager.cs b/MediaBrowser.Controller/Syncplay/ISyncplayManager.cs new file mode 100644 index 000000000..ec91ea69d --- /dev/null +++ b/MediaBrowser.Controller/Syncplay/ISyncplayManager.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Syncplay; + +namespace MediaBrowser.Controller.Syncplay +{ + /// + /// Interface ISyncplayManager. + /// + public interface ISyncplayManager + { + /// + /// Creates a new group. + /// + /// The user that's creating the group. + void NewGroup(SessionInfo user); + + /// + /// Adds the user to a group. + /// + /// The session. + /// The group id. + void JoinGroup(SessionInfo user, string groupId); + + /// + /// Removes the user from a group. + /// + /// The session. + void LeaveGroup(SessionInfo user); + + /// + /// Gets list of available groups for a user. + /// + /// The user. + /// The list of available groups. + List ListGroups(SessionInfo user); + + /// + /// Handle a request by a user in a group. + /// + /// The session. + /// The request. + void HandleRequest(SessionInfo user, SyncplayRequestInfo request); + + /// + /// Maps a user to a group. + /// + /// The user. + /// The group. + /// + void MapUserToGroup(SessionInfo user, ISyncplayController group); + + /// + /// Unmaps a user from a group. + /// + /// The user. + /// The group. + /// + void UnmapUserFromGroup(SessionInfo user, ISyncplayController group); + } +} diff --git a/MediaBrowser.Model/Syncplay/GroupInfoModel.cs b/MediaBrowser.Model/Syncplay/GroupInfoModel.cs new file mode 100644 index 000000000..599c0dbfc --- /dev/null +++ b/MediaBrowser.Model/Syncplay/GroupInfoModel.cs @@ -0,0 +1,38 @@ +namespace MediaBrowser.Model.Syncplay +{ + /// + /// Class GroupInfoModel. + /// + public class GroupInfoView + { + /// + /// Gets or sets the group identifier. + /// + /// The group identifier. + public string GroupId { get; set; } + + /// + /// Gets or sets the playing item id. + /// + /// The playing item id. + public string PlayingItemId { get; set; } + + /// + /// Gets or sets the playing item name. + /// + /// The playing item name. + public string PlayingItemName { get; set; } + + /// + /// Gets or sets the position ticks. + /// + /// The position ticks. + public long PositionTicks { get; set; } + + /// + /// Gets or sets the partecipants. + /// + /// The partecipants. + public string[] Partecipants { get; set; } + } +} diff --git a/MediaBrowser.Model/Syncplay/SyncplayCommand.cs b/MediaBrowser.Model/Syncplay/SyncplayCommand.cs new file mode 100644 index 000000000..769316e80 --- /dev/null +++ b/MediaBrowser.Model/Syncplay/SyncplayCommand.cs @@ -0,0 +1,32 @@ +namespace MediaBrowser.Model.Syncplay +{ + /// + /// Class SyncplayCommand. + /// + public class SyncplayCommand + { + /// + /// Gets or sets the group identifier. + /// + /// The group identifier. + public string GroupId { get; set; } + + /// + /// Gets or sets the UTC time when to execute the command. + /// + /// The UTC time when to execute the command. + public string When { get; set; } + + /// + /// Gets or sets the position ticks. + /// + /// The position ticks. + public long? PositionTicks { get; set; } + + /// + /// Gets or sets the command. + /// + /// The command. + public SyncplayCommandType Command { get; set; } + } +} diff --git a/MediaBrowser.Model/Syncplay/SyncplayCommandType.cs b/MediaBrowser.Model/Syncplay/SyncplayCommandType.cs new file mode 100644 index 000000000..87b9ad66d --- /dev/null +++ b/MediaBrowser.Model/Syncplay/SyncplayCommandType.cs @@ -0,0 +1,21 @@ +namespace MediaBrowser.Model.Syncplay +{ + /// + /// Enum SyncplayCommandType. + /// + public enum SyncplayCommandType + { + /// + /// The play command. Instructs users to start playback. + /// + Play = 0, + /// + /// The pause command. Instructs users to pause playback. + /// + Pause = 1, + /// + /// The seek command. Instructs users to seek to a specified time. + /// + Seek = 2 + } +} diff --git a/MediaBrowser.Model/Syncplay/SyncplayGroupUpdate.cs b/MediaBrowser.Model/Syncplay/SyncplayGroupUpdate.cs new file mode 100644 index 000000000..c5c2f3540 --- /dev/null +++ b/MediaBrowser.Model/Syncplay/SyncplayGroupUpdate.cs @@ -0,0 +1,26 @@ +namespace MediaBrowser.Model.Syncplay +{ + /// + /// Class SyncplayGroupUpdate. + /// + public class SyncplayGroupUpdate + { + /// + /// Gets or sets the group identifier. + /// + /// The group identifier. + public string GroupId { get; set; } + + /// + /// Gets or sets the update type. + /// + /// The update type. + public SyncplayGroupUpdateType Type { get; set; } + + /// + /// Gets or sets the data. + /// + /// The data. + public T Data { get; set; } + } +} diff --git a/MediaBrowser.Model/Syncplay/SyncplayGroupUpdateType.cs b/MediaBrowser.Model/Syncplay/SyncplayGroupUpdateType.cs new file mode 100644 index 000000000..c7c5f534d --- /dev/null +++ b/MediaBrowser.Model/Syncplay/SyncplayGroupUpdateType.cs @@ -0,0 +1,41 @@ +namespace MediaBrowser.Model.Syncplay +{ + /// + /// Enum SyncplayGroupUpdateType + /// + public enum SyncplayGroupUpdateType + { + /// + /// The user-joined update. Tells members of a group about a new user. + /// + UserJoined = 0, + /// + /// The user-left update. Tells members of a group that a user left. + /// + UserLeft = 1, + /// + /// The group-joined update. Tells a user that the group has been joined. + /// + GroupJoined = 2, + /// + /// The group-left update. Tells a user that the group has been left. + /// + GroupLeft = 3, + /// + /// The group-wait update. Tells members of the group that a user is buffering. + /// + GroupWait = 4, + /// + /// The prepare-session update. Tells a user to load some content. + /// + PrepareSession = 5, + /// + /// The keep-alive update. An update to keep alive the socket. + /// + KeepAlive = 6, + /// + /// The not-in-group update. Tells a user that no group has been joined. + /// + NotInGroup = 7 + } +} diff --git a/MediaBrowser.Model/Syncplay/SyncplayRequestInfo.cs b/MediaBrowser.Model/Syncplay/SyncplayRequestInfo.cs new file mode 100644 index 000000000..7dba74ae9 --- /dev/null +++ b/MediaBrowser.Model/Syncplay/SyncplayRequestInfo.cs @@ -0,0 +1,34 @@ +using System; + +namespace MediaBrowser.Model.Syncplay +{ + /// + /// Class SyncplayRequestInfo. + /// + public class SyncplayRequestInfo + { + /// + /// Gets or sets the request type. + /// + /// The request type. + public SyncplayRequestType Type; + + /// + /// Gets or sets when the request has been made by the client. + /// + /// The date of the request. + public DateTime? When { get; set; } + + /// + /// Gets or sets the position ticks. + /// + /// The position ticks. + public long? PositionTicks { get; set; } + + /// + /// Gets or sets the ping time. + /// + /// The ping time. + public long? Ping { get; set; } + } +} diff --git a/MediaBrowser.Model/Syncplay/SyncplayRequestType.cs b/MediaBrowser.Model/Syncplay/SyncplayRequestType.cs new file mode 100644 index 000000000..44d7a0af2 --- /dev/null +++ b/MediaBrowser.Model/Syncplay/SyncplayRequestType.cs @@ -0,0 +1,33 @@ +namespace MediaBrowser.Model.Syncplay +{ + /// + /// Enum SyncplayRequestType + /// + public enum SyncplayRequestType + { + /// + /// A user is requesting a play command for the group. + /// + Play = 0, + /// + /// A user is requesting a pause command for the group. + /// + Pause = 1, + /// + /// A user is requesting a seek command for the group. + /// + Seek = 2, + /// + /// A user is signaling that playback is buffering. + /// + Buffering = 3, + /// + /// A user is signaling that playback resumed. + /// + BufferingComplete = 4, + /// + /// A user is reporting its ping. + /// + KeepAlive = 5 + } +} -- cgit v1.2.3 From 2aaecb8e148aef6cda67797fa4227a8ebcf7e5bb Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Tue, 28 Apr 2020 21:45:46 +0100 Subject: Whilst fixing issues with SSDP on devices with multiple interfaces, i came across a design issue in the current code - namely interfaces without a gateway were ignored. Fixing this required the removal of the code that attempted to detect virtual interfaces. Not wanting to remove functionality, but not able to keep the code in place, I implemented a work around solution (see 4 below). Whilst in the area, I also fixed a few minor bugs i encountered (1, 5, 6 below) and stopped SSDP messages from going out on non-LAN interfaces (3) All these changes are related. Changes 1 IsInPrivateAddressSpace - improved subnet code checking 2 interfaces with no gateway were being excluded from SSDP blasts 3 filtered SSDP blasts from not LAN addresses as defined on the network page. 4 removed #986 mod - as this was part of the issue of #2986. Interfaces can be excluded from the LAN by putting the LAN address in brackets. eg. [10.1.1.1] will exclude an interface with ip address 10.1.1.1 from SSDP 5 fixed a problem where an invalid LAN address causing the SSDP to crash 6 corrected local link filter (FilterIPAddress) to filter on 169.254. addresses --- Emby.Dlna/Main/DlnaEntryPoint.cs | 6 + Emby.Server.Implementations/ApplicationHost.cs | 2 +- .../Networking/NetworkManager.cs | 149 ++++++++++++--------- MediaBrowser.Common/Net/INetworkManager.cs | 4 +- RSSDP/SsdpCommunicationsServer.cs | 4 +- RSSDP/SsdpDeviceLocator.cs | 2 +- 6 files changed, 97 insertions(+), 70 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index c5d60b2a0..721350483 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -266,6 +266,12 @@ namespace Emby.Dlna.Main continue; } + // Limit to LAN addresses only + if (!_networkManager.IsAddressInSubnets(address, true, true)) + { + continue; + } + var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 33aec1a06..97fc2c004 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1274,7 +1274,7 @@ namespace Emby.Server.Implementations if (addresses.Count == 0) { - addresses.AddRange(_networkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces)); + addresses.AddRange(_networkManager.GetLocalIpAddresses()); } var resultList = new List(); diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs index b3e88b667..465530888 100644 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ b/Emby.Server.Implementations/Networking/NetworkManager.cs @@ -56,13 +56,13 @@ namespace Emby.Server.Implementations.Networking NetworkChanged?.Invoke(this, EventArgs.Empty); } - public IPAddress[] GetLocalIpAddresses(bool ignoreVirtualInterface = true) + public IPAddress[] GetLocalIpAddresses() { lock (_localIpAddressSyncLock) { if (_localIpAddresses == null) { - var addresses = GetLocalIpAddressesInternal(ignoreVirtualInterface).ToArray(); + var addresses = GetLocalIpAddressesInternal().ToArray(); _localIpAddresses = addresses; } @@ -71,35 +71,37 @@ namespace Emby.Server.Implementations.Networking } } - private List GetLocalIpAddressesInternal(bool ignoreVirtualInterface) + private List GetLocalIpAddressesInternal() { - var list = GetIPsDefault(ignoreVirtualInterface).ToList(); + var list = GetIPsDefault().ToList(); if (list.Count == 0) { list = GetLocalIpAddressesFallback().GetAwaiter().GetResult().ToList(); } - var listClone = list.ToList(); + var listClone = new List(); - return list - .OrderBy(i => i.AddressFamily == AddressFamily.InterNetwork ? 0 : 1) - .ThenBy(i => listClone.IndexOf(i)) - .Where(FilterIpAddress) - .GroupBy(i => i.ToString()) - .Select(x => x.First()) - .ToList(); - } + var subnets = LocalSubnetsFn(); - private static bool FilterIpAddress(IPAddress address) - { - if (address.IsIPv6LinkLocal - || address.ToString().StartsWith("169.", StringComparison.OrdinalIgnoreCase)) + foreach (var i in list) { - return false; + if (i.IsIPv6LinkLocal || i.ToString().StartsWith("169.254.", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + if (Array.IndexOf(subnets, "[" + i.ToString() + "]") == -1) + { + listClone.Add(i); + } } - return true; + return listClone + .OrderBy(i => i.AddressFamily == AddressFamily.InterNetwork ? 0 : 1) + // .ThenBy(i => listClone.IndexOf(i)) + .GroupBy(i => i.ToString()) + .Select(x => x.First()) + .ToList(); } public bool IsInPrivateAddressSpace(string endpoint) @@ -107,6 +109,7 @@ namespace Emby.Server.Implementations.Networking return IsInPrivateAddressSpace(endpoint, true); } + // checks if the address in endpoint is an RFC1918, RFC1122, or RFC3927 address private bool IsInPrivateAddressSpace(string endpoint, bool checkSubnets) { if (string.Equals(endpoint, "::1", StringComparison.OrdinalIgnoreCase)) @@ -128,23 +131,28 @@ namespace Emby.Server.Implementations.Networking } // Private address space: - // http://en.wikipedia.org/wiki/Private_network - if (endpoint.StartsWith("172.", StringComparison.OrdinalIgnoreCase)) + if (endpoint.ToLower() == "localhost") { - return Is172AddressPrivate(endpoint); + return true; } - if (endpoint.StartsWith("localhost", StringComparison.OrdinalIgnoreCase) || - endpoint.StartsWith("127.", StringComparison.OrdinalIgnoreCase) || - endpoint.StartsWith("169.", StringComparison.OrdinalIgnoreCase)) + try { - return true; - } + byte[] octet = IPAddress.Parse(endpoint).GetAddressBytes(); - if (checkSubnets && endpoint.StartsWith("192.168", StringComparison.OrdinalIgnoreCase)) + if ((octet[0] == 10) || + (octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918 + (octet[0] == 192 && octet[1] == 168) || // RFC1918 + (octet[0] == 127) || // RFC1122 + (octet[0] == 169 && octet[1] == 254)) // RFC3927 + { + return false; + } + } + catch { - return true; + // return false; } if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint)) @@ -177,6 +185,7 @@ namespace Emby.Server.Implementations.Networking return false; } + // Gives a list of possible subnets from the system whose interface ip starts with endpointFirstPart private List GetSubnets(string endpointFirstPart) { lock (_subnetLookupLock) @@ -222,19 +231,6 @@ namespace Emby.Server.Implementations.Networking } } - private static bool Is172AddressPrivate(string endpoint) - { - for (var i = 16; i <= 31; i++) - { - if (endpoint.StartsWith("172." + i.ToString(CultureInfo.InvariantCulture) + ".", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; - } - public bool IsInLocalNetwork(string endpoint) { return IsInLocalNetworkInternal(endpoint, true); @@ -245,23 +241,57 @@ namespace Emby.Server.Implementations.Networking return IsAddressInSubnets(IPAddress.Parse(addressString), addressString, subnets); } + // returns true if address is in the LAN list in the config file + // always returns false if address has been excluded from the LAN if excludeInterfaces is true + // and excludes RFC addresses if excludeRFC is true + public bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC) + { + byte[] octet = address.GetAddressBytes(); + + if ((octet[0] == 127) || // RFC1122 + (octet[0] == 169 && octet[1] == 254)) // RFC3927 + { + // don't use on loopback or 169 interfaces + return false; + } + + string addressString = address.ToString(); + string excludeAddress = "[" + addressString + "]"; + var subnets = LocalSubnetsFn(); + + // Exclude any addresses if they appear in the LAN list in [ ] + if (Array.IndexOf(subnets, excludeAddress) != -1) + { + return false; + } + return IsAddressInSubnets(address, addressString, subnets); + } + + // Checks to see if address/addressString (same but different type) falls within subnets[] private static bool IsAddressInSubnets(IPAddress address, string addressString, string[] subnets) { foreach (var subnet in subnets) { var normalizedSubnet = subnet.Trim(); - + // is the subnet a host address and does it match the address being passes? if (string.Equals(normalizedSubnet, addressString, StringComparison.OrdinalIgnoreCase)) { return true; } - + // parse CIDR subnets and see if address falls within it. if (normalizedSubnet.Contains('/', StringComparison.Ordinal)) { - var ipNetwork = IPNetwork.Parse(normalizedSubnet); - if (ipNetwork.Contains(address)) + try { - return true; + var ipNetwork = IPNetwork.Parse(normalizedSubnet); + if (ipNetwork.Contains(address)) + { + return true; + } + } + catch + { + // Ignoring - invalid subnet passed encountered. } } } @@ -359,8 +389,8 @@ namespace Emby.Server.Implementations.Networking { return Dns.GetHostAddressesAsync(hostName); } - - private IEnumerable GetIPsDefault(bool ignoreVirtualInterface) + + private IEnumerable GetIPsDefault() { IEnumerable interfaces; @@ -380,15 +410,7 @@ namespace Emby.Server.Implementations.Networking { var ipProperties = network.GetIPProperties(); - // Try to exclude virtual adapters - // http://stackoverflow.com/questions/8089685/c-sharp-finding-my-machines-local-ip-address-and-not-the-vms - var addr = ipProperties.GatewayAddresses.FirstOrDefault(); - if (addr == null - || (ignoreVirtualInterface - && (addr.Address.Equals(IPAddress.Any) || addr.Address.Equals(IPAddress.IPv6Any)))) - { - return Enumerable.Empty(); - } + // Exclude any addresses if they appear in the LAN list in [ ] return ipProperties.UnicastAddresses .Select(i => i.Address) @@ -494,15 +516,12 @@ namespace Emby.Server.Implementations.Networking foreach (NetworkInterface ni in interfaces) { - if (ni.GetIPProperties().GatewayAddresses.FirstOrDefault() != null) + foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses) { - foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses) + if (ip.Address.Equals(address) && ip.IPv4Mask != null) { - if (ip.Address.Equals(address) && ip.IPv4Mask != null) - { - return ip.IPv4Mask; - } - } + return ip.IPv4Mask; + } } } diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index 3ba75abd8..b7ec1d122 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -41,10 +41,12 @@ namespace MediaBrowser.Common.Net /// true if [is in local network] [the specified endpoint]; otherwise, false. bool IsInLocalNetwork(string endpoint); - IPAddress[] GetLocalIpAddresses(bool ignoreVirtualInterface); + IPAddress[] GetLocalIpAddresses(); bool IsAddressInSubnets(string addressString, string[] subnets); + bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC); + bool IsInSameSubnet(IPAddress address1, IPAddress address2, IPAddress subnetMask); IPAddress GetLocalIpSubnetMask(IPAddress address); diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 18097ef24..47da52005 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -370,13 +370,13 @@ namespace Rssdp.Infrastructure if (_enableMultiSocketBinding) { - foreach (var address in _networkManager.GetLocalIpAddresses(_config.Configuration.IgnoreVirtualInterfaces)) + foreach (var address in _networkManager.GetLocalIpAddresses()) { if (address.AddressFamily == AddressFamily.InterNetworkV6) { // Not support IPv6 right now continue; - } + } try { diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs index 59a2710d5..b62c50e28 100644 --- a/RSSDP/SsdpDeviceLocator.cs +++ b/RSSDP/SsdpDeviceLocator.cs @@ -357,7 +357,7 @@ namespace Rssdp.Infrastructure private void ProcessSearchResponseMessage(HttpResponseMessage message, IPAddress localIpAddress) { if (!message.IsSuccessStatusCode) return; - + var location = GetFirstHeaderUriValue("Location", message); if (location != null) { -- cgit v1.2.3 From a3140f83c6461164658303d1bb7c1d992cfd9802 Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Tue, 28 Apr 2020 21:51:49 +0100 Subject: Revert "Whilst fixing issues with SSDP on devices with multiple interfaces, i came across a design issue in the current code - namely interfaces without a gateway were ignored." This reverts commit 2aaecb8e148aef6cda67797fa4227a8ebcf7e5bb. --- Emby.Dlna/Main/DlnaEntryPoint.cs | 6 - Emby.Server.Implementations/ApplicationHost.cs | 2 +- .../Networking/NetworkManager.cs | 149 +++++++++------------ MediaBrowser.Common/Net/INetworkManager.cs | 4 +- RSSDP/SsdpCommunicationsServer.cs | 4 +- RSSDP/SsdpDeviceLocator.cs | 2 +- 6 files changed, 70 insertions(+), 97 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 721350483..c5d60b2a0 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -266,12 +266,6 @@ namespace Emby.Dlna.Main continue; } - // Limit to LAN addresses only - if (!_networkManager.IsAddressInSubnets(address, true, true)) - { - continue; - } - var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 97fc2c004..33aec1a06 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1274,7 +1274,7 @@ namespace Emby.Server.Implementations if (addresses.Count == 0) { - addresses.AddRange(_networkManager.GetLocalIpAddresses()); + addresses.AddRange(_networkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces)); } var resultList = new List(); diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs index 465530888..b3e88b667 100644 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ b/Emby.Server.Implementations/Networking/NetworkManager.cs @@ -56,13 +56,13 @@ namespace Emby.Server.Implementations.Networking NetworkChanged?.Invoke(this, EventArgs.Empty); } - public IPAddress[] GetLocalIpAddresses() + public IPAddress[] GetLocalIpAddresses(bool ignoreVirtualInterface = true) { lock (_localIpAddressSyncLock) { if (_localIpAddresses == null) { - var addresses = GetLocalIpAddressesInternal().ToArray(); + var addresses = GetLocalIpAddressesInternal(ignoreVirtualInterface).ToArray(); _localIpAddresses = addresses; } @@ -71,45 +71,42 @@ namespace Emby.Server.Implementations.Networking } } - private List GetLocalIpAddressesInternal() + private List GetLocalIpAddressesInternal(bool ignoreVirtualInterface) { - var list = GetIPsDefault().ToList(); + var list = GetIPsDefault(ignoreVirtualInterface).ToList(); if (list.Count == 0) { list = GetLocalIpAddressesFallback().GetAwaiter().GetResult().ToList(); } - var listClone = new List(); + var listClone = list.ToList(); - var subnets = LocalSubnetsFn(); - - foreach (var i in list) - { - if (i.IsIPv6LinkLocal || i.ToString().StartsWith("169.254.", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - if (Array.IndexOf(subnets, "[" + i.ToString() + "]") == -1) - { - listClone.Add(i); - } - } - - return listClone + return list .OrderBy(i => i.AddressFamily == AddressFamily.InterNetwork ? 0 : 1) - // .ThenBy(i => listClone.IndexOf(i)) + .ThenBy(i => listClone.IndexOf(i)) + .Where(FilterIpAddress) .GroupBy(i => i.ToString()) .Select(x => x.First()) .ToList(); } + private static bool FilterIpAddress(IPAddress address) + { + if (address.IsIPv6LinkLocal + || address.ToString().StartsWith("169.", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return true; + } + public bool IsInPrivateAddressSpace(string endpoint) { return IsInPrivateAddressSpace(endpoint, true); } - // checks if the address in endpoint is an RFC1918, RFC1122, or RFC3927 address private bool IsInPrivateAddressSpace(string endpoint, bool checkSubnets) { if (string.Equals(endpoint, "::1", StringComparison.OrdinalIgnoreCase)) @@ -131,28 +128,23 @@ namespace Emby.Server.Implementations.Networking } // Private address space: + // http://en.wikipedia.org/wiki/Private_network - if (endpoint.ToLower() == "localhost") + if (endpoint.StartsWith("172.", StringComparison.OrdinalIgnoreCase)) { - return true; + return Is172AddressPrivate(endpoint); } - try + if (endpoint.StartsWith("localhost", StringComparison.OrdinalIgnoreCase) || + endpoint.StartsWith("127.", StringComparison.OrdinalIgnoreCase) || + endpoint.StartsWith("169.", StringComparison.OrdinalIgnoreCase)) { - byte[] octet = IPAddress.Parse(endpoint).GetAddressBytes(); - - if ((octet[0] == 10) || - (octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918 - (octet[0] == 192 && octet[1] == 168) || // RFC1918 - (octet[0] == 127) || // RFC1122 - (octet[0] == 169 && octet[1] == 254)) // RFC3927 - { - return false; - } + return true; } - catch + + if (checkSubnets && endpoint.StartsWith("192.168", StringComparison.OrdinalIgnoreCase)) { - // return false; + return true; } if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint)) @@ -185,7 +177,6 @@ namespace Emby.Server.Implementations.Networking return false; } - // Gives a list of possible subnets from the system whose interface ip starts with endpointFirstPart private List GetSubnets(string endpointFirstPart) { lock (_subnetLookupLock) @@ -231,6 +222,19 @@ namespace Emby.Server.Implementations.Networking } } + private static bool Is172AddressPrivate(string endpoint) + { + for (var i = 16; i <= 31; i++) + { + if (endpoint.StartsWith("172." + i.ToString(CultureInfo.InvariantCulture) + ".", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + public bool IsInLocalNetwork(string endpoint) { return IsInLocalNetworkInternal(endpoint, true); @@ -241,57 +245,23 @@ namespace Emby.Server.Implementations.Networking return IsAddressInSubnets(IPAddress.Parse(addressString), addressString, subnets); } - // returns true if address is in the LAN list in the config file - // always returns false if address has been excluded from the LAN if excludeInterfaces is true - // and excludes RFC addresses if excludeRFC is true - public bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC) - { - byte[] octet = address.GetAddressBytes(); - - if ((octet[0] == 127) || // RFC1122 - (octet[0] == 169 && octet[1] == 254)) // RFC3927 - { - // don't use on loopback or 169 interfaces - return false; - } - - string addressString = address.ToString(); - string excludeAddress = "[" + addressString + "]"; - var subnets = LocalSubnetsFn(); - - // Exclude any addresses if they appear in the LAN list in [ ] - if (Array.IndexOf(subnets, excludeAddress) != -1) - { - return false; - } - return IsAddressInSubnets(address, addressString, subnets); - } - - // Checks to see if address/addressString (same but different type) falls within subnets[] private static bool IsAddressInSubnets(IPAddress address, string addressString, string[] subnets) { foreach (var subnet in subnets) { var normalizedSubnet = subnet.Trim(); - // is the subnet a host address and does it match the address being passes? + if (string.Equals(normalizedSubnet, addressString, StringComparison.OrdinalIgnoreCase)) { return true; } - // parse CIDR subnets and see if address falls within it. + if (normalizedSubnet.Contains('/', StringComparison.Ordinal)) { - try - { - var ipNetwork = IPNetwork.Parse(normalizedSubnet); - if (ipNetwork.Contains(address)) - { - return true; - } - } - catch + var ipNetwork = IPNetwork.Parse(normalizedSubnet); + if (ipNetwork.Contains(address)) { - // Ignoring - invalid subnet passed encountered. + return true; } } } @@ -389,8 +359,8 @@ namespace Emby.Server.Implementations.Networking { return Dns.GetHostAddressesAsync(hostName); } - - private IEnumerable GetIPsDefault() + + private IEnumerable GetIPsDefault(bool ignoreVirtualInterface) { IEnumerable interfaces; @@ -410,7 +380,15 @@ namespace Emby.Server.Implementations.Networking { var ipProperties = network.GetIPProperties(); - // Exclude any addresses if they appear in the LAN list in [ ] + // Try to exclude virtual adapters + // http://stackoverflow.com/questions/8089685/c-sharp-finding-my-machines-local-ip-address-and-not-the-vms + var addr = ipProperties.GatewayAddresses.FirstOrDefault(); + if (addr == null + || (ignoreVirtualInterface + && (addr.Address.Equals(IPAddress.Any) || addr.Address.Equals(IPAddress.IPv6Any)))) + { + return Enumerable.Empty(); + } return ipProperties.UnicastAddresses .Select(i => i.Address) @@ -516,12 +494,15 @@ namespace Emby.Server.Implementations.Networking foreach (NetworkInterface ni in interfaces) { - foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses) + if (ni.GetIPProperties().GatewayAddresses.FirstOrDefault() != null) { - if (ip.Address.Equals(address) && ip.IPv4Mask != null) + foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses) { - return ip.IPv4Mask; - } + if (ip.Address.Equals(address) && ip.IPv4Mask != null) + { + return ip.IPv4Mask; + } + } } } diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index b7ec1d122..3ba75abd8 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -41,12 +41,10 @@ namespace MediaBrowser.Common.Net /// true if [is in local network] [the specified endpoint]; otherwise, false. bool IsInLocalNetwork(string endpoint); - IPAddress[] GetLocalIpAddresses(); + IPAddress[] GetLocalIpAddresses(bool ignoreVirtualInterface); bool IsAddressInSubnets(string addressString, string[] subnets); - bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC); - bool IsInSameSubnet(IPAddress address1, IPAddress address2, IPAddress subnetMask); IPAddress GetLocalIpSubnetMask(IPAddress address); diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 47da52005..18097ef24 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -370,13 +370,13 @@ namespace Rssdp.Infrastructure if (_enableMultiSocketBinding) { - foreach (var address in _networkManager.GetLocalIpAddresses()) + foreach (var address in _networkManager.GetLocalIpAddresses(_config.Configuration.IgnoreVirtualInterfaces)) { if (address.AddressFamily == AddressFamily.InterNetworkV6) { // Not support IPv6 right now continue; - } + } try { diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs index b62c50e28..59a2710d5 100644 --- a/RSSDP/SsdpDeviceLocator.cs +++ b/RSSDP/SsdpDeviceLocator.cs @@ -357,7 +357,7 @@ namespace Rssdp.Infrastructure private void ProcessSearchResponseMessage(HttpResponseMessage message, IPAddress localIpAddress) { if (!message.IsSuccessStatusCode) return; - + var location = GetFirstHeaderUriValue("Location", message); if (location != null) { -- cgit v1.2.3 From ebd589aa86abb7bdbc9ab981cb8f4c908f790ac1 Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Tue, 28 Apr 2020 21:57:39 +0100 Subject: Whilst fixing issues with SSDP on devices with multiple interfaces, i came across a design issue in the current code - namely interfaces without a gateway were ignored. Fixing this required the removal of the code that attempted to detect virtual interfaces. Not wanting to remove functionality, but not able to keep the code in place, I implemented a work around solution (see 4 below). Whilst in the area, I also fixed a few minor bugs i encountered (1, 5, 6 below) and stopped SSDP messages from going out on non-LAN interfaces (3) All these changes are related. Changes 1 IsInPrivateAddressSpace - improved subnet code checking 2 interfaces with no gateway were being excluded from SSDP blasts 3 filtered SSDP blasts from not LAN addresses as defined on the network page. 4 removed #986 mod - as this was part of the issue of #2986. Interfaces can be excluded from the LAN by putting the LAN address in brackets. eg. [10.1.1.1] will exclude an interface with ip address 10.1.1.1 from SSDP 5 fixed a problem where an invalid LAN address causing the SSDP to crash 6 corrected local link filter (FilterIPAddress) to filter on 169.254. addresses --- Emby.Dlna/Main/DlnaEntryPoint.cs | 8 +- Emby.Server.Implementations/ApplicationHost.cs | 2 +- .../Networking/NetworkManager.cs | 150 ++++++++++++--------- MediaBrowser.Common/Net/INetworkManager.cs | 4 +- RSSDP/SsdpCommunicationsServer.cs | 11 +- RSSDP/SsdpDeviceLocator.cs | 2 +- 6 files changed, 101 insertions(+), 76 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index c5d60b2a0..e6806c87b 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -180,7 +180,7 @@ namespace Emby.Dlna.Main var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows || OperatingSystem.Id == OperatingSystemId.Linux; - _communicationsServer = new SsdpCommunicationsServer(_config, _socketFactory, _networkManager, _logger, enableMultiSocketBinding) + _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding) { IsShared = true }; @@ -266,6 +266,12 @@ namespace Emby.Dlna.Main continue; } + // Limit to LAN addresses only + if (!_networkManager.IsAddressInSubnets(address, true, true)) + { + continue; + } + var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 33aec1a06..97fc2c004 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1274,7 +1274,7 @@ namespace Emby.Server.Implementations if (addresses.Count == 0) { - addresses.AddRange(_networkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces)); + addresses.AddRange(_networkManager.GetLocalIpAddresses()); } var resultList = new List(); diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs index b3e88b667..5979d1eae 100644 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ b/Emby.Server.Implementations/Networking/NetworkManager.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Net; using System.Net.NetworkInformation; @@ -56,13 +55,13 @@ namespace Emby.Server.Implementations.Networking NetworkChanged?.Invoke(this, EventArgs.Empty); } - public IPAddress[] GetLocalIpAddresses(bool ignoreVirtualInterface = true) + public IPAddress[] GetLocalIpAddresses() { lock (_localIpAddressSyncLock) { if (_localIpAddresses == null) { - var addresses = GetLocalIpAddressesInternal(ignoreVirtualInterface).ToArray(); + var addresses = GetLocalIpAddressesInternal().ToArray(); _localIpAddresses = addresses; } @@ -71,35 +70,37 @@ namespace Emby.Server.Implementations.Networking } } - private List GetLocalIpAddressesInternal(bool ignoreVirtualInterface) + private List GetLocalIpAddressesInternal() { - var list = GetIPsDefault(ignoreVirtualInterface).ToList(); + var list = GetIPsDefault().ToList(); if (list.Count == 0) { list = GetLocalIpAddressesFallback().GetAwaiter().GetResult().ToList(); } - var listClone = list.ToList(); + var listClone = new List(); - return list - .OrderBy(i => i.AddressFamily == AddressFamily.InterNetwork ? 0 : 1) - .ThenBy(i => listClone.IndexOf(i)) - .Where(FilterIpAddress) - .GroupBy(i => i.ToString()) - .Select(x => x.First()) - .ToList(); - } + var subnets = LocalSubnetsFn(); - private static bool FilterIpAddress(IPAddress address) - { - if (address.IsIPv6LinkLocal - || address.ToString().StartsWith("169.", StringComparison.OrdinalIgnoreCase)) + foreach (var i in list) { - return false; + if (i.IsIPv6LinkLocal || i.ToString().StartsWith("169.254.", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + if (Array.IndexOf(subnets, "[" + i.ToString() + "]") == -1) + { + listClone.Add(i); + } } - return true; + return listClone + .OrderBy(i => i.AddressFamily == AddressFamily.InterNetwork ? 0 : 1) + // .ThenBy(i => listClone.IndexOf(i)) + .GroupBy(i => i.ToString()) + .Select(x => x.First()) + .ToList(); } public bool IsInPrivateAddressSpace(string endpoint) @@ -107,6 +108,7 @@ namespace Emby.Server.Implementations.Networking return IsInPrivateAddressSpace(endpoint, true); } + // checks if the address in endpoint is an RFC1918, RFC1122, or RFC3927 address private bool IsInPrivateAddressSpace(string endpoint, bool checkSubnets) { if (string.Equals(endpoint, "::1", StringComparison.OrdinalIgnoreCase)) @@ -128,23 +130,28 @@ namespace Emby.Server.Implementations.Networking } // Private address space: - // http://en.wikipedia.org/wiki/Private_network - if (endpoint.StartsWith("172.", StringComparison.OrdinalIgnoreCase)) + if (endpoint.ToLower() == "localhost") { - return Is172AddressPrivate(endpoint); + return true; } - if (endpoint.StartsWith("localhost", StringComparison.OrdinalIgnoreCase) || - endpoint.StartsWith("127.", StringComparison.OrdinalIgnoreCase) || - endpoint.StartsWith("169.", StringComparison.OrdinalIgnoreCase)) + try { - return true; - } + byte[] octet = IPAddress.Parse(endpoint).GetAddressBytes(); - if (checkSubnets && endpoint.StartsWith("192.168", StringComparison.OrdinalIgnoreCase)) + if ((octet[0] == 10) || + (octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918 + (octet[0] == 192 && octet[1] == 168) || // RFC1918 + (octet[0] == 127) || // RFC1122 + (octet[0] == 169 && octet[1] == 254)) // RFC3927 + { + return false; + } + } + catch { - return true; + } if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint)) @@ -177,6 +184,7 @@ namespace Emby.Server.Implementations.Networking return false; } + // Gives a list of possible subnets from the system whose interface ip starts with endpointFirstPart private List GetSubnets(string endpointFirstPart) { lock (_subnetLookupLock) @@ -222,19 +230,6 @@ namespace Emby.Server.Implementations.Networking } } - private static bool Is172AddressPrivate(string endpoint) - { - for (var i = 16; i <= 31; i++) - { - if (endpoint.StartsWith("172." + i.ToString(CultureInfo.InvariantCulture) + ".", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; - } - public bool IsInLocalNetwork(string endpoint) { return IsInLocalNetworkInternal(endpoint, true); @@ -245,23 +240,57 @@ namespace Emby.Server.Implementations.Networking return IsAddressInSubnets(IPAddress.Parse(addressString), addressString, subnets); } + // returns true if address is in the LAN list in the config file + // always returns false if address has been excluded from the LAN if excludeInterfaces is true + // and excludes RFC addresses if excludeRFC is true + public bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC) + { + byte[] octet = address.GetAddressBytes(); + + if ((octet[0] == 127) || // RFC1122 + (octet[0] == 169 && octet[1] == 254)) // RFC3927 + { + // don't use on loopback or 169 interfaces + return false; + } + + string addressString = address.ToString(); + string excludeAddress = "[" + addressString + "]"; + var subnets = LocalSubnetsFn(); + + // Exclude any addresses if they appear in the LAN list in [ ] + if (Array.IndexOf(subnets, excludeAddress) != -1) + { + return false; + } + return IsAddressInSubnets(address, addressString, subnets); + } + + // Checks to see if address/addressString (same but different type) falls within subnets[] private static bool IsAddressInSubnets(IPAddress address, string addressString, string[] subnets) { foreach (var subnet in subnets) { var normalizedSubnet = subnet.Trim(); - + // is the subnet a host address and does it match the address being passes? if (string.Equals(normalizedSubnet, addressString, StringComparison.OrdinalIgnoreCase)) { return true; } - + // parse CIDR subnets and see if address falls within it. if (normalizedSubnet.Contains('/', StringComparison.Ordinal)) { - var ipNetwork = IPNetwork.Parse(normalizedSubnet); - if (ipNetwork.Contains(address)) + try { - return true; + var ipNetwork = IPNetwork.Parse(normalizedSubnet); + if (ipNetwork.Contains(address)) + { + return true; + } + } + catch + { + // Ignoring - invalid subnet passed encountered. } } } @@ -359,8 +388,8 @@ namespace Emby.Server.Implementations.Networking { return Dns.GetHostAddressesAsync(hostName); } - - private IEnumerable GetIPsDefault(bool ignoreVirtualInterface) + + private IEnumerable GetIPsDefault() { IEnumerable interfaces; @@ -380,15 +409,7 @@ namespace Emby.Server.Implementations.Networking { var ipProperties = network.GetIPProperties(); - // Try to exclude virtual adapters - // http://stackoverflow.com/questions/8089685/c-sharp-finding-my-machines-local-ip-address-and-not-the-vms - var addr = ipProperties.GatewayAddresses.FirstOrDefault(); - if (addr == null - || (ignoreVirtualInterface - && (addr.Address.Equals(IPAddress.Any) || addr.Address.Equals(IPAddress.IPv6Any)))) - { - return Enumerable.Empty(); - } + // Exclude any addresses if they appear in the LAN list in [ ] return ipProperties.UnicastAddresses .Select(i => i.Address) @@ -494,15 +515,12 @@ namespace Emby.Server.Implementations.Networking foreach (NetworkInterface ni in interfaces) { - if (ni.GetIPProperties().GatewayAddresses.FirstOrDefault() != null) + foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses) { - foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses) + if (ip.Address.Equals(address) && ip.IPv4Mask != null) { - if (ip.Address.Equals(address) && ip.IPv4Mask != null) - { - return ip.IPv4Mask; - } - } + return ip.IPv4Mask; + } } } diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index 3ba75abd8..b7ec1d122 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -41,10 +41,12 @@ namespace MediaBrowser.Common.Net /// true if [is in local network] [the specified endpoint]; otherwise, false. bool IsInLocalNetwork(string endpoint); - IPAddress[] GetLocalIpAddresses(bool ignoreVirtualInterface); + IPAddress[] GetLocalIpAddresses(); bool IsAddressInSubnets(string addressString, string[] subnets); + bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC); + bool IsInSameSubnet(IPAddress address1, IPAddress address2, IPAddress subnetMask); IPAddress GetLocalIpSubnetMask(IPAddress address); diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 18097ef24..a16e4c73f 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -46,8 +46,7 @@ namespace Rssdp.Infrastructure private HttpResponseParser _ResponseParser; private readonly ILogger _logger; private ISocketFactory _SocketFactory; - private readonly INetworkManager _networkManager; - private readonly IServerConfigurationManager _config; + private readonly INetworkManager _networkManager; private int _LocalPort; private int _MulticastTtl; @@ -77,11 +76,11 @@ namespace Rssdp.Infrastructure /// Minimum constructor. /// /// The argument is null. - public SsdpCommunicationsServer(IServerConfigurationManager config, ISocketFactory socketFactory, + public SsdpCommunicationsServer(ISocketFactory socketFactory, INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding) : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger, enableMultiSocketBinding) { - _config = config; + } /// @@ -370,13 +369,13 @@ namespace Rssdp.Infrastructure if (_enableMultiSocketBinding) { - foreach (var address in _networkManager.GetLocalIpAddresses(_config.Configuration.IgnoreVirtualInterfaces)) + foreach (var address in _networkManager.GetLocalIpAddresses()) { if (address.AddressFamily == AddressFamily.InterNetworkV6) { // Not support IPv6 right now continue; - } + } try { diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs index 59a2710d5..b62c50e28 100644 --- a/RSSDP/SsdpDeviceLocator.cs +++ b/RSSDP/SsdpDeviceLocator.cs @@ -357,7 +357,7 @@ namespace Rssdp.Infrastructure private void ProcessSearchResponseMessage(HttpResponseMessage message, IPAddress localIpAddress) { if (!message.IsSuccessStatusCode) return; - + var location = GetFirstHeaderUriValue("Location", message); if (location != null) { -- cgit v1.2.3 From 04f826e50c23a8a996fd317124260f67b7ff3f9a Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sat, 2 May 2020 01:09:35 +0200 Subject: Fix merge errors --- Emby.Server.Implementations/ApplicationHost.cs | 3 +- .../HttpServer/HttpListenerHost.cs | 9 +- .../HttpServer/IHttpListener.cs | 39 ------ .../Net/WebSocketConnectEventArgs.cs | 29 ----- .../SocketSharp/WebSocketSharpListener.cs | 135 --------------------- .../WebSockets/WebSocketManager.cs | 102 ---------------- 6 files changed, 4 insertions(+), 313 deletions(-) delete mode 100644 Emby.Server.Implementations/HttpServer/IHttpListener.cs delete mode 100644 Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs delete mode 100644 Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs delete mode 100644 Emby.Server.Implementations/WebSockets/WebSocketManager.cs (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 8b387e195..6279ce5d0 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -504,7 +504,7 @@ namespace Emby.Server.Implementations } public Task ExecuteHttpHandlerAsync(HttpContext context, Func next) - => HttpServer.RequestHandler(context); + => _httpServer.RequestHandler(context); /// /// Registers services/resources with the service collection that will be available via DI. @@ -597,7 +597,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 2648b57d5..e75140d6c 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -53,7 +53,6 @@ namespace Emby.Server.Implementations.HttpServer private readonly string _baseUrlPrefix; private readonly Dictionary _serviceOperationsMap = new Dictionary(); - private readonly List _webSocketConnections = new List(); private readonly IHostEnvironment _hostEnvironment; private IWebSocketListener[] _webSocketListeners = Array.Empty(); @@ -67,10 +66,10 @@ namespace Emby.Server.Implementations.HttpServer INetworkManager networkManager, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, - IHttpListener socketListener, ILocalizationManager localizationManager, ServiceController serviceController, - IHostEnvironment hostEnvironment) + IHostEnvironment hostEnvironment, + ILoggerFactory loggerFactory) { _appHost = applicationHost; _logger = logger; @@ -80,11 +79,9 @@ namespace Emby.Server.Implementations.HttpServer _networkManager = networkManager; _jsonSerializer = jsonSerializer; _xmlSerializer = xmlSerializer; - _socketListener = socketListener; ServiceController = serviceController; - - _socketListener.WebSocketConnected = OnWebSocketConnected; _hostEnvironment = hostEnvironment; + _loggerFactory = loggerFactory; _funcParseFn = t => s => JsvReader.GetParseFn(t)(s); diff --git a/Emby.Server.Implementations/HttpServer/IHttpListener.cs b/Emby.Server.Implementations/HttpServer/IHttpListener.cs deleted file mode 100644 index 501593725..000000000 --- a/Emby.Server.Implementations/HttpServer/IHttpListener.cs +++ /dev/null @@ -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 - { - /// - /// Gets or sets the error handler. - /// - /// The error handler. - Func ErrorHandler { get; set; } - - /// - /// Gets or sets the request handler. - /// - /// The request handler. - Func RequestHandler { get; set; } - - /// - /// Gets or sets the web socket handler. - /// - /// The web socket handler. - Action WebSocketConnected { get; set; } - - /// - /// Stops this instance. - /// - Task Stop(); - - Task ProcessWebSocketRequest(HttpContext ctx); - } -} diff --git a/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs b/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs deleted file mode 100644 index 6880766f9..000000000 --- a/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using Microsoft.AspNetCore.Http; - -namespace Emby.Server.Implementations.Net -{ - public class WebSocketConnectEventArgs : EventArgs - { - /// - /// Gets or sets the URL. - /// - /// The URL. - public string Url { get; set; } - /// - /// Gets or sets the query string. - /// - /// The query string. - public IQueryCollection QueryString { get; set; } - /// - /// Gets or sets the web socket. - /// - /// The web socket. - public IWebSocket WebSocket { get; set; } - /// - /// Gets or sets the endpoint. - /// - /// The endpoint. - public string Endpoint { get; set; } - } -} diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs deleted file mode 100644 index b85750c9b..000000000 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs +++ /dev/null @@ -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 logger) - { - _logger = logger; - _disposeCancellationToken = _disposeCancellationTokenSource.Token; - } - - public Func ErrorHandler { get; set; } - - public Func RequestHandler { get; set; } - - public Action 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(); - - 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; - } - - /// - /// Releases the unmanaged resources and disposes of the managed resources used. - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private bool _disposed; - - /// - /// Releases the unmanaged resources and disposes of the managed resources used. - /// - /// Whether or not the managed resources should be disposed. - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - Stop().GetAwaiter().GetResult(); - } - - _disposed = true; - } - } -} diff --git a/Emby.Server.Implementations/WebSockets/WebSocketManager.cs b/Emby.Server.Implementations/WebSockets/WebSocketManager.cs deleted file mode 100644 index 31a7468fb..000000000 --- a/Emby.Server.Implementations/WebSockets/WebSocketManager.cs +++ /dev/null @@ -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 _logger; - private const int BufferSize = 4096; - - public WebSocketManager(IWebSocketHandler[] webSocketHandlers, IJsonSerializer jsonSerializer, ILogger logger) - { - _webSocketHandlers = webSocketHandlers; - _jsonSerializer = jsonSerializer; - _logger = logger; - } - - public async Task OnWebSocketConnected(WebSocket webSocket) - { - var taskCompletionSource = new TaskCompletionSource(); - var cancellationToken = new CancellationTokenSource().Token; - WebSocketReceiveResult result; - var message = new List(); - - // 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 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>(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"); - } - } - } -} -- cgit v1.2.3 From 3623aafcb60dec4f4f5055046717d895b7597b60 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sat, 2 May 2020 01:30:04 +0200 Subject: Make SonarCloud happy --- Emby.Server.Implementations/ApplicationHost.cs | 5 +---- .../HttpServer/HttpListenerHost.cs | 25 +--------------------- .../HttpServer/WebSocketConnection.cs | 8 ------- .../Session/WebSocketController.cs | 2 +- MediaBrowser.Controller/Net/IHttpServer.cs | 5 ++--- 5 files changed, 5 insertions(+), 40 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 6279ce5d0..11fed24f7 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -93,7 +93,6 @@ using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using MediaBrowser.Model.System; using MediaBrowser.Model.Tasks; -using MediaBrowser.Model.Updates; using MediaBrowser.Providers.Chapters; using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.TheTvdb; @@ -101,12 +100,10 @@ using MediaBrowser.Providers.Subtitles; using MediaBrowser.WebDashboard.Api; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; using Prometheus.DotNetRuntime; +using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; namespace Emby.Server.Implementations { diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index e75140d6c..7358c1a81 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -28,12 +28,11 @@ using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; using ServiceStack.Text.Jsv; namespace Emby.Server.Implementations.HttpServer { - public class HttpListenerHost : IHttpServer, IDisposable + public class HttpListenerHost : IHttpServer { /// /// The key for a setting that specifies the default redirect path @@ -699,28 +698,6 @@ namespace Emby.Server.Implementations.HttpServer return _baseUrlPrefix + NormalizeUrlPath(path); } - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - // TODO: - } - - _disposed = true; - } - /// /// Processes the web socket message received. /// diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index 1af748ebc..095725c50 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -36,8 +36,6 @@ namespace Emby.Server.Implementations.HttpServer /// private readonly WebSocket _socket; - private bool _disposed = false; - /// /// Initializes a new instance of the class. /// @@ -221,12 +219,6 @@ namespace Emby.Server.Implementations.HttpServer }; await OnReceive(info).ConfigureAwait(false); - - // Stop reading if there's no more data coming - if (result.IsCompleted) - { - return; - } } } } diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs index c7ef9b1ce..a0274acd2 100644 --- a/Emby.Server.Implementations/Session/WebSocketController.cs +++ b/Emby.Server.Implementations/Session/WebSocketController.cs @@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.Session private readonly ISessionManager _sessionManager; private readonly SessionInfo _session; - private List _sockets; + private readonly List _sockets; private bool _disposed = false; public WebSocketController( diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs index 666ac1cfe..f1c441761 100644 --- a/MediaBrowser.Controller/Net/IHttpServer.cs +++ b/MediaBrowser.Controller/Net/IHttpServer.cs @@ -2,15 +2,14 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using MediaBrowser.Model.Events; -using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net { /// - /// Interface IHttpServer + /// Interface IHttpServer. /// - public interface IHttpServer : IDisposable + public interface IHttpServer { /// /// Gets the URL prefix. -- cgit v1.2.3 From 032de931b14ded24bb1098a7eeec3d84561206e2 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sat, 2 May 2020 18:32:22 -0400 Subject: Migrate activity db to EF Core --- .../Activity/ActivityLogEntryPoint.cs | 288 ++-- .../Activity/ActivityManager.cs | 70 - .../Activity/ActivityRepository.cs | 308 ---- Emby.Server.Implementations/ApplicationHost.cs | 14 +- .../Emby.Server.Implementations.csproj | 5 +- Jellyfin.Data/DbContexts/Jellyfin.cs | 1140 --------------- Jellyfin.Data/Entities/ActivityLog.cs | 153 ++ Jellyfin.Data/ISavingChanges.cs | 9 + Jellyfin.Data/Jellyfin.Data.csproj | 24 +- .../Activity/ActivityManager.cs | 103 ++ .../Jellyfin.Server.Implementations.csproj | 34 + Jellyfin.Server.Implementations/JellyfinDb.cs | 119 ++ .../JellyfinDbProvider.cs | 33 + .../20200430215054_InitialSchema.Designer.cs | 1513 ++++++++++++++++++++ .../Migrations/20200430215054_InitialSchema.cs | 1294 +++++++++++++++++ .../Migrations/DesignTimeJellyfinDbFactory.cs | 20 + .../Migrations/JellyfinDbModelSnapshot.cs | 1511 +++++++++++++++++++ Jellyfin.Server/Jellyfin.Server.csproj | 7 + Jellyfin.Server/Migrations/MigrationRunner.cs | 3 +- .../Migrations/Routines/MigrateActivityLogDb.cs | 109 ++ MediaBrowser.Api/Library/LibraryService.cs | 11 +- MediaBrowser.Api/System/ActivityLogService.cs | 2 +- MediaBrowser.Model/Activity/ActivityLogEntry.cs | 1 + MediaBrowser.Model/Activity/IActivityManager.cs | 15 +- MediaBrowser.Model/Activity/IActivityRepository.cs | 14 - MediaBrowser.Model/MediaBrowser.Model.csproj | 3 + MediaBrowser.sln | 46 +- 27 files changed, 5147 insertions(+), 1702 deletions(-) delete mode 100644 Emby.Server.Implementations/Activity/ActivityManager.cs delete mode 100644 Emby.Server.Implementations/Activity/ActivityRepository.cs delete mode 100644 Jellyfin.Data/DbContexts/Jellyfin.cs create mode 100644 Jellyfin.Data/Entities/ActivityLog.cs create mode 100644 Jellyfin.Data/ISavingChanges.cs create mode 100644 Jellyfin.Server.Implementations/Activity/ActivityManager.cs create mode 100644 Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj create mode 100644 Jellyfin.Server.Implementations/JellyfinDb.cs create mode 100644 Jellyfin.Server.Implementations/JellyfinDbProvider.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20200430215054_InitialSchema.Designer.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20200430215054_InitialSchema.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs create mode 100644 Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs delete mode 100644 MediaBrowser.Model/Activity/IActivityRepository.cs (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs index 4685a03ac..54894fd65 100644 --- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs +++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs @@ -4,11 +4,11 @@ using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; @@ -104,47 +104,53 @@ namespace Emby.Server.Implementations.Activity return Task.CompletedTask; } - private void OnCameraImageUploaded(object sender, GenericEventArgs e) + private async void OnCameraImageUploaded(object sender, GenericEventArgs e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("CameraImageUploadedFrom"), e.Argument.Device.Name), - Type = NotificationType.CameraImageUploaded.ToString() - }); + NotificationType.CameraImageUploaded.ToString(), + Guid.Empty, + DateTime.UtcNow, + LogLevel.Trace)) + .ConfigureAwait(false); } - private void OnUserLockedOut(object sender, GenericEventArgs e) + private async void OnUserLockedOut(object sender, GenericEventArgs e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserLockedOutWithName"), e.Argument.Name), - Type = NotificationType.UserLockedOut.ToString(), - UserId = e.Argument.Id - }); + NotificationType.UserLockedOut.ToString(), + e.Argument.Id, + DateTime.UtcNow, + LogLevel.Trace)) + .ConfigureAwait(false); } - private void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e) + private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"), e.Provider, - Notifications.NotificationEntryPoint.GetItemName(e.Item)), - Type = "SubtitleDownloadFailure", + Emby.Notifications.NotificationEntryPoint.GetItemName(e.Item)), + "SubtitleDownloadFailure", + Guid.Empty, + DateTime.UtcNow, + LogLevel.Trace) + { ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture), ShortOverview = e.Exception.Message - }); + }).ConfigureAwait(false); } - private void OnPlaybackStopped(object sender, PlaybackStopEventArgs e) + private async void OnPlaybackStopped(object sender, PlaybackStopEventArgs e) { var item = e.MediaInfo; @@ -167,20 +173,21 @@ namespace Emby.Server.Implementations.Activity var user = e.Users[0]; - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserStoppedPlayingItemWithValues"), user.Name, GetItemName(item), e.DeviceName), - Type = GetPlaybackStoppedNotificationType(item.MediaType), - UserId = user.Id - }); + GetPlaybackStoppedNotificationType(item.MediaType), + user.Id, + DateTime.UtcNow, + LogLevel.Trace)) + .ConfigureAwait(false); } - private void OnPlaybackStart(object sender, PlaybackProgressEventArgs e) + private async void OnPlaybackStart(object sender, PlaybackProgressEventArgs e) { var item = e.MediaInfo; @@ -203,17 +210,18 @@ namespace Emby.Server.Implementations.Activity var user = e.Users.First(); - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserStartedPlayingItemWithValues"), user.Name, GetItemName(item), e.DeviceName), - Type = GetPlaybackNotificationType(item.MediaType), - UserId = user.Id - }); + GetPlaybackNotificationType(item.MediaType), + user.Id, + DateTime.UtcNow, + LogLevel.Trace)) + .ConfigureAwait(false); } private static string GetItemName(BaseItemDto item) @@ -263,7 +271,7 @@ namespace Emby.Server.Implementations.Activity return null; } - private void OnSessionEnded(object sender, SessionEventArgs e) + private async void OnSessionEnded(object sender, SessionEventArgs e) { var session = e.SessionInfo; @@ -272,110 +280,120 @@ namespace Emby.Server.Implementations.Activity return; } - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserOfflineFromDevice"), session.UserName, session.DeviceName), - Type = "SessionEnded", + "SessionEnded", + session.UserId, + DateTime.UtcNow, + LogLevel.Trace) + { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("LabelIpAddressValue"), session.RemoteEndPoint), - UserId = session.UserId - }); + }).ConfigureAwait(false); } - private void OnAuthenticationSucceeded(object sender, GenericEventArgs e) + private async void OnAuthenticationSucceeded(object sender, GenericEventArgs e) { var user = e.Argument.User; - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("AuthenticationSucceededWithUserName"), user.Name), - Type = "AuthenticationSucceeded", + "AuthenticationSucceeded", + user.Id, + DateTime.UtcNow, + LogLevel.Trace) + { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("LabelIpAddressValue"), e.Argument.SessionInfo.RemoteEndPoint), - UserId = user.Id - }); + }).ConfigureAwait(false); } - private void OnAuthenticationFailed(object sender, GenericEventArgs e) + private async void OnAuthenticationFailed(object sender, GenericEventArgs e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("FailedLoginAttemptWithUserName"), e.Argument.Username), - Type = "AuthenticationFailed", + "AuthenticationFailed", + Guid.Empty, + DateTime.UtcNow, + LogLevel.Error) + { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("LabelIpAddressValue"), e.Argument.RemoteEndPoint), - Severity = LogLevel.Error - }); + }).ConfigureAwait(false); } - private void OnUserPolicyUpdated(object sender, GenericEventArgs e) + private async void OnUserPolicyUpdated(object sender, GenericEventArgs e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserPolicyUpdatedWithName"), e.Argument.Name), - Type = "UserPolicyUpdated", - UserId = e.Argument.Id - }); + "UserPolicyUpdated", + e.Argument.Id, + DateTime.UtcNow, + LogLevel.Trace)) + .ConfigureAwait(false); } - private void OnUserDeleted(object sender, GenericEventArgs e) + private async void OnUserDeleted(object sender, GenericEventArgs e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDeletedWithName"), e.Argument.Name), - Type = "UserDeleted" - }); + "UserDeleted", + Guid.Empty, + DateTime.UtcNow, + LogLevel.Trace)) + .ConfigureAwait(false); } - private void OnUserPasswordChanged(object sender, GenericEventArgs e) + private async void OnUserPasswordChanged(object sender, GenericEventArgs e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserPasswordChangedWithName"), e.Argument.Name), - Type = "UserPasswordChanged", - UserId = e.Argument.Id - }); + "UserPasswordChanged", + e.Argument.Id, + DateTime.UtcNow, + LogLevel.Trace)).ConfigureAwait(false); } - private void OnUserCreated(object sender, GenericEventArgs e) + private async void OnUserCreated(object sender, GenericEventArgs e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserCreatedWithName"), e.Argument.Name), - Type = "UserCreated", - UserId = e.Argument.Id - }); + "UserCreated", + e.Argument.Id, + DateTime.UtcNow, + LogLevel.Trace)) + .ConfigureAwait(false); } - private void OnSessionStarted(object sender, SessionEventArgs e) + private async void OnSessionStarted(object sender, SessionEventArgs e) { var session = e.SessionInfo; @@ -384,87 +402,100 @@ namespace Emby.Server.Implementations.Activity return; } - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserOnlineFromDevice"), session.UserName, session.DeviceName), - Type = "SessionStarted", + "SessionStarted", + session.UserId, + DateTime.UtcNow, + LogLevel.Trace) + { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("LabelIpAddressValue"), - session.RemoteEndPoint), - UserId = session.UserId - }); + session.RemoteEndPoint) + }).ConfigureAwait(false); } - private void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, VersionInfo)> e) + private async void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, VersionInfo)> e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("PluginUpdatedWithName"), e.Argument.Item1.Name), - Type = NotificationType.PluginUpdateInstalled.ToString(), + NotificationType.PluginUpdateInstalled.ToString(), + Guid.Empty, + DateTime.UtcNow, + LogLevel.Trace) + { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("VersionNumber"), e.Argument.Item2.version), Overview = e.Argument.Item2.changelog - }); + }).ConfigureAwait(false); } - private void OnPluginUninstalled(object sender, GenericEventArgs e) + private async void OnPluginUninstalled(object sender, GenericEventArgs e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("PluginUninstalledWithName"), e.Argument.Name), - Type = NotificationType.PluginUninstalled.ToString() - }); + NotificationType.PluginUninstalled.ToString(), + Guid.Empty, + DateTime.UtcNow, + LogLevel.Trace)) + .ConfigureAwait(false); } - private void OnPluginInstalled(object sender, GenericEventArgs e) + private async void OnPluginInstalled(object sender, GenericEventArgs e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("PluginInstalledWithName"), e.Argument.name), - Type = NotificationType.PluginInstalled.ToString(), + NotificationType.PluginInstalled.ToString(), + Guid.Empty, + DateTime.UtcNow, + LogLevel.Trace) + { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("VersionNumber"), e.Argument.version) - }); + }).ConfigureAwait(false); } - private void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e) + private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e) { var installationInfo = e.InstallationInfo; - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("NameInstallFailed"), installationInfo.Name), - Type = NotificationType.InstallationFailed.ToString(), + NotificationType.InstallationFailed.ToString(), + Guid.Empty, + DateTime.UtcNow, + LogLevel.Trace) + { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("VersionNumber"), installationInfo.Version), Overview = e.Exception.Message - }); + }).ConfigureAwait(false); } - private void OnTaskCompleted(object sender, TaskCompletionEventArgs e) + private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e) { var result = e.Result; var task = e.Task; @@ -495,22 +526,21 @@ namespace Emby.Server.Implementations.Activity vals.Add(e.Result.LongErrorMessage); } - CreateLogEntry(new ActivityLogEntry + await CreateLogEntry(new ActivityLog( + string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name), + NotificationType.TaskFailed.ToString(), + Guid.Empty, + DateTime.UtcNow, + LogLevel.Error) { - Name = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("ScheduledTaskFailedWithName"), - task.Name), - Type = NotificationType.TaskFailed.ToString(), Overview = string.Join(Environment.NewLine, vals), - ShortOverview = runningTime, - Severity = LogLevel.Error - }); + ShortOverview = runningTime + }).ConfigureAwait(false); } } - private void CreateLogEntry(ActivityLogEntry entry) - => _activityManager.Create(entry); + private async Task CreateLogEntry(ActivityLog entry) + => await _activityManager.CreateAsync(entry).ConfigureAwait(false); /// public void Dispose() @@ -558,7 +588,7 @@ namespace Emby.Server.Implementations.Activity { int years = days / DaysInYear; values.Add(CreateValueString(years, "year")); - days %= DaysInYear; + days = days % DaysInYear; } // Number of months @@ -566,7 +596,7 @@ namespace Emby.Server.Implementations.Activity { int months = days / DaysInMonth; values.Add(CreateValueString(months, "month")); - days %= DaysInMonth; + days = days % DaysInMonth; } // Number of days diff --git a/Emby.Server.Implementations/Activity/ActivityManager.cs b/Emby.Server.Implementations/Activity/ActivityManager.cs deleted file mode 100644 index 81bebae3d..000000000 --- a/Emby.Server.Implementations/Activity/ActivityManager.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Activity; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Querying; - -namespace Emby.Server.Implementations.Activity -{ - /// - /// The activity log manager. - /// - public class ActivityManager : IActivityManager - { - private readonly IActivityRepository _repo; - private readonly IUserManager _userManager; - - /// - /// Initializes a new instance of the class. - /// - /// The activity repository. - /// The user manager. - public ActivityManager(IActivityRepository repo, IUserManager userManager) - { - _repo = repo; - _userManager = userManager; - } - - /// - public event EventHandler> EntryCreated; - - public void Create(ActivityLogEntry entry) - { - entry.Date = DateTime.UtcNow; - - _repo.Create(entry); - - EntryCreated?.Invoke(this, new GenericEventArgs(entry)); - } - - /// - public QueryResult GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? startIndex, int? limit) - { - var result = _repo.GetActivityLogEntries(minDate, hasUserId, startIndex, limit); - - foreach (var item in result.Items) - { - if (item.UserId == Guid.Empty) - { - continue; - } - - var user = _userManager.GetUserById(item.UserId); - - if (user != null) - { - var dto = _userManager.GetUserDto(user); - item.UserPrimaryImageTag = dto.PrimaryImageTag; - } - } - - return result; - } - - /// - public QueryResult GetActivityLogEntries(DateTime? minDate, int? startIndex, int? limit) - { - return GetActivityLogEntries(minDate, null, startIndex, limit); - } - } -} diff --git a/Emby.Server.Implementations/Activity/ActivityRepository.cs b/Emby.Server.Implementations/Activity/ActivityRepository.cs deleted file mode 100644 index 22796ba3f..000000000 --- a/Emby.Server.Implementations/Activity/ActivityRepository.cs +++ /dev/null @@ -1,308 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using Emby.Server.Implementations.Data; -using MediaBrowser.Controller; -using MediaBrowser.Model.Activity; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Querying; -using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; - -namespace Emby.Server.Implementations.Activity -{ - /// - /// The activity log repository. - /// - public class ActivityRepository : BaseSqliteRepository, IActivityRepository - { - private const string BaseActivitySelectText = "select Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity from ActivityLog"; - - private readonly IFileSystem _fileSystem; - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - /// The server application paths. - /// The filesystem. - public ActivityRepository(ILogger logger, IServerApplicationPaths appPaths, IFileSystem fileSystem) - : base(logger) - { - DbFilePath = Path.Combine(appPaths.DataPath, "activitylog.db"); - _fileSystem = fileSystem; - } - - /// - /// Initializes the . - /// - public void Initialize() - { - try - { - InitializeInternal(); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error loading database file. Will reset and retry."); - - _fileSystem.DeleteFile(DbFilePath); - - InitializeInternal(); - } - } - - private void InitializeInternal() - { - using var connection = GetConnection(); - connection.RunQueries(new[] - { - "create table if not exists ActivityLog (Id INTEGER PRIMARY KEY, Name TEXT NOT NULL, Overview TEXT, ShortOverview TEXT, Type TEXT NOT NULL, ItemId TEXT, UserId TEXT, DateCreated DATETIME NOT NULL, LogSeverity TEXT NOT NULL)", - "drop index if exists idx_ActivityLogEntries" - }); - - TryMigrate(connection); - } - - private void TryMigrate(ManagedConnection connection) - { - try - { - if (TableExists(connection, "ActivityLogEntries")) - { - connection.RunQueries(new[] - { - "INSERT INTO ActivityLog (Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) SELECT Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity FROM ActivityLogEntries", - "drop table if exists ActivityLogEntries" - }); - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Error migrating activity log database"); - } - } - - /// - public void Create(ActivityLogEntry entry) - { - if (entry == null) - { - throw new ArgumentNullException(nameof(entry)); - } - - using var connection = GetConnection(); - connection.RunInTransaction(db => - { - using var statement = db.PrepareStatement("insert into ActivityLog (Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) values (@Name, @Overview, @ShortOverview, @Type, @ItemId, @UserId, @DateCreated, @LogSeverity)"); - statement.TryBind("@Name", entry.Name); - - statement.TryBind("@Overview", entry.Overview); - statement.TryBind("@ShortOverview", entry.ShortOverview); - statement.TryBind("@Type", entry.Type); - statement.TryBind("@ItemId", entry.ItemId); - - if (entry.UserId.Equals(Guid.Empty)) - { - statement.TryBindNull("@UserId"); - } - else - { - statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture)); - } - - statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue()); - statement.TryBind("@LogSeverity", entry.Severity.ToString()); - - statement.MoveNext(); - }, TransactionMode); - } - - /// - /// Adds the provided to this repository. - /// - /// The activity log entry. - /// If entry is null. - public void Update(ActivityLogEntry entry) - { - if (entry == null) - { - throw new ArgumentNullException(nameof(entry)); - } - - using var connection = GetConnection(); - connection.RunInTransaction(db => - { - using var statement = db.PrepareStatement("Update ActivityLog set Name=@Name,Overview=@Overview,ShortOverview=@ShortOverview,Type=@Type,ItemId=@ItemId,UserId=@UserId,DateCreated=@DateCreated,LogSeverity=@LogSeverity where Id=@Id"); - statement.TryBind("@Id", entry.Id); - - statement.TryBind("@Name", entry.Name); - statement.TryBind("@Overview", entry.Overview); - statement.TryBind("@ShortOverview", entry.ShortOverview); - statement.TryBind("@Type", entry.Type); - statement.TryBind("@ItemId", entry.ItemId); - - if (entry.UserId.Equals(Guid.Empty)) - { - statement.TryBindNull("@UserId"); - } - else - { - statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture)); - } - - statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue()); - statement.TryBind("@LogSeverity", entry.Severity.ToString()); - - statement.MoveNext(); - }, TransactionMode); - } - - /// - public QueryResult GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? startIndex, int? limit) - { - var commandText = BaseActivitySelectText; - var whereClauses = new List(); - - if (minDate.HasValue) - { - whereClauses.Add("DateCreated>=@DateCreated"); - } - - if (hasUserId.HasValue) - { - whereClauses.Add(hasUserId.Value ? "UserId not null" : "UserId is null"); - } - - var whereTextWithoutPaging = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - if (startIndex.HasValue && startIndex.Value > 0) - { - var pagingWhereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - whereClauses.Add( - string.Format( - CultureInfo.InvariantCulture, - "Id NOT IN (SELECT Id FROM ActivityLog {0} ORDER BY DateCreated DESC LIMIT {1})", - pagingWhereText, - startIndex.Value)); - } - - var whereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - commandText += whereText; - - commandText += " ORDER BY DateCreated DESC"; - - if (limit.HasValue) - { - commandText += " LIMIT " + limit.Value.ToString(CultureInfo.InvariantCulture); - } - - var statementTexts = new[] - { - commandText, - "select count (Id) from ActivityLog" + whereTextWithoutPaging - }; - - var list = new List(); - var result = new QueryResult(); - - using var connection = GetConnection(true); - connection.RunInTransaction( - db => - { - var statements = PrepareAll(db, statementTexts).ToList(); - - using (var statement = statements[0]) - { - if (minDate.HasValue) - { - statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue()); - } - - list.AddRange(statement.ExecuteQuery().Select(GetEntry)); - } - - using (var statement = statements[1]) - { - if (minDate.HasValue) - { - statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue()); - } - - result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); - } - }, - ReadTransactionMode); - - result.Items = list; - return result; - } - - private static ActivityLogEntry GetEntry(IReadOnlyList reader) - { - var index = 0; - - var info = new ActivityLogEntry - { - Id = reader[index].ToInt64() - }; - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.Name = reader[index].ToString(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.Overview = reader[index].ToString(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.ShortOverview = reader[index].ToString(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.Type = reader[index].ToString(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.ItemId = reader[index].ToString(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.UserId = new Guid(reader[index].ToString()); - } - - index++; - info.Date = reader[index].ReadDateTime(); - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.Severity = Enum.Parse(reader[index].ToString(), true); - } - - return info; - } - } -} diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index ffc916b98..ddd9c7953 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -22,7 +22,6 @@ using Emby.Dlna.Ssdp; using Emby.Drawing; using Emby.Notifications; using Emby.Photos; -using Emby.Server.Implementations.Activity; using Emby.Server.Implementations.Archiving; using Emby.Server.Implementations.Channels; using Emby.Server.Implementations.Collections; @@ -47,6 +46,8 @@ using Emby.Server.Implementations.Session; using Emby.Server.Implementations.SocketSharp; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; +using Jellyfin.Server.Implementations; +using Jellyfin.Server.Implementations.Activity; using MediaBrowser.Api; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; @@ -94,7 +95,6 @@ using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using MediaBrowser.Model.System; using MediaBrowser.Model.Tasks; -using MediaBrowser.Model.Updates; using MediaBrowser.Providers.Chapters; using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.TheTvdb; @@ -103,6 +103,7 @@ using MediaBrowser.WebDashboard.Api; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; @@ -553,6 +554,13 @@ namespace Emby.Server.Implementations return Logger; }); + // TODO: properly set up scoping and switch to AddDbContextPool + serviceCollection.AddDbContext( + options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"), + ServiceLifetime.Transient); + + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(_fileSystemManager); serviceCollection.AddSingleton(); @@ -663,7 +671,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -696,7 +703,6 @@ namespace Emby.Server.Implementations ((SqliteDisplayPreferencesRepository)Resolve()).Initialize(); ((AuthenticationRepository)Resolve()).Initialize(); ((SqliteUserRepository)Resolve()).Initialize(); - ((ActivityRepository)Resolve()).Initialize(); SetStaticProperties(); diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 44fc932e3..dccbe2a9a 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -1,4 +1,4 @@ - + @@ -9,6 +9,7 @@ + @@ -50,7 +51,7 @@ - netstandard2.1 + netcoreapp3.1 false true diff --git a/Jellyfin.Data/DbContexts/Jellyfin.cs b/Jellyfin.Data/DbContexts/Jellyfin.cs deleted file mode 100644 index fd488ce7d..000000000 --- a/Jellyfin.Data/DbContexts/Jellyfin.cs +++ /dev/null @@ -1,1140 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated from a template. -// -// Manual changes to this file may cause unexpected behavior in your application. -// Manual changes to this file will be overwritten if the code is regenerated. -// -// Produced by Entity Framework Visual Editor -// https://github.com/msawczyn/EFDesigner -// -//------------------------------------------------------------------------------ - -using System; -using System.Collections.Generic; -using System.Linq; -using System.ComponentModel.DataAnnotations.Schema; -using Microsoft.EntityFrameworkCore; - -namespace Jellyfin.Data.DbContexts -{ - /// - public partial class Jellyfin : DbContext - { - #region DbSets - public virtual Microsoft.EntityFrameworkCore.DbSet Artwork { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet Books { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet BookMetadata { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet Chapters { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet Collections { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet CollectionItems { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet Companies { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet CompanyMetadata { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet CustomItems { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet CustomItemMetadata { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet Episodes { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet EpisodeMetadata { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet Genres { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet Groups { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet Libraries { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet LibraryItems { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet LibraryRoot { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet MediaFiles { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet MediaFileStream { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet Metadata { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet MetadataProviders { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet MetadataProviderIds { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet Movies { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet MovieMetadata { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet MusicAlbums { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet MusicAlbumMetadata { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet Permissions { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet People { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet PersonRoles { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet Photo { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet PhotoMetadata { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet Preferences { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet ProviderMappings { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet Ratings { get; set; } - - /// - /// Repository for global::Jellyfin.Data.Entities.RatingSource - This is the entity to - /// store review ratings, not age ratings - /// - public virtual Microsoft.EntityFrameworkCore.DbSet RatingSources { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet Releases { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet Seasons { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet SeasonMetadata { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet Series { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet SeriesMetadata { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet Tracks { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet TrackMetadata { get; set; } - public virtual Microsoft.EntityFrameworkCore.DbSet Users { get; set; } - #endregion DbSets - - /// - /// Default connection string - /// - public static string ConnectionString { get; set; } = @"Data Source=jellyfin.db"; - - /// - public Jellyfin(DbContextOptions options) : base(options) - { - } - - partial void CustomInit(DbContextOptionsBuilder optionsBuilder); - - /// - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - CustomInit(optionsBuilder); - } - - partial void OnModelCreatingImpl(ModelBuilder modelBuilder); - partial void OnModelCreatedImpl(ModelBuilder modelBuilder); - - /// - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - OnModelCreatingImpl(modelBuilder); - - modelBuilder.HasDefaultSchema("jellyfin"); - - modelBuilder.Entity() - .ToTable("Artwork") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .HasField("_Id") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.Path) - .HasMaxLength(65535) - .IsRequired() - .HasField("_Path") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Kind) - .IsRequired() - .HasField("_Kind") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity().HasIndex(t => t.Kind); - modelBuilder.Entity() - .Property(t => t.Timestamp) - .IsRequired() - .HasField("_Timestamp") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .IsRowVersion(); - - modelBuilder.Entity() - .HasMany(x => x.BookMetadata) - .WithOne() - .HasForeignKey("BookMetadata_BookMetadata_Id") - .IsRequired(); - modelBuilder.Entity() - .HasMany(x => x.Releases) - .WithOne() - .HasForeignKey("Release_Releases_Id") - .IsRequired(); - - modelBuilder.Entity() - .Property(t => t.ISBN) - .HasField("_ISBN") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .HasMany(x => x.Publishers) - .WithOne() - .HasForeignKey("Company_Publishers_Id") - .IsRequired(); - - modelBuilder.Entity() - .ToTable("Chapter") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .HasField("_Id") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.Name) - .HasMaxLength(1024) - .HasField("_Name") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Language) - .HasMaxLength(3) - .IsRequired() - .HasField("_Language") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.TimeStart) - .IsRequired() - .HasField("_TimeStart") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.TimeEnd) - .HasField("_TimeEnd") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Timestamp) - .IsRequired() - .HasField("_Timestamp") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .IsRowVersion(); - - modelBuilder.Entity() - .ToTable("Collection") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .HasField("_Id") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.Name) - .HasMaxLength(1024) - .HasField("_Name") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Timestamp) - .IsRequired() - .HasField("_Timestamp") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .IsRowVersion(); - modelBuilder.Entity() - .HasMany(x => x.CollectionItem) - .WithOne() - .HasForeignKey("CollectionItem_CollectionItem_Id") - .IsRequired(); - - modelBuilder.Entity() - .ToTable("CollectionItem") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .HasField("_Id") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.Timestamp) - .IsRequired() - .HasField("_Timestamp") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .IsRowVersion(); - modelBuilder.Entity() - .HasOne(x => x.LibraryItem) - .WithOne() - .HasForeignKey("LibraryItem_Id") - .IsRequired(); - modelBuilder.Entity() - .HasOne(x => x.Next) - .WithOne() - .HasForeignKey("CollectionItem_Next_Id") - .IsRequired(); - modelBuilder.Entity() - .HasOne(x => x.Previous) - .WithOne() - .HasForeignKey("CollectionItem_Previous_Id") - .IsRequired(); - - modelBuilder.Entity() - .ToTable("Company") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .HasField("_Id") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.Timestamp) - .IsRequired() - .HasField("_Timestamp") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .IsRowVersion(); - modelBuilder.Entity() - .HasMany(x => x.CompanyMetadata) - .WithOne() - .HasForeignKey("CompanyMetadata_CompanyMetadata_Id") - .IsRequired(); - modelBuilder.Entity() - .HasOne(x => x.Parent) - .WithOne() - .HasForeignKey("Company_Parent_Id") - .IsRequired(); - - modelBuilder.Entity() - .Property(t => t.Description) - .HasMaxLength(65535) - .HasField("_Description") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Headquarters) - .HasMaxLength(255) - .HasField("_Headquarters") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Country) - .HasMaxLength(2) - .HasField("_Country") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Homepage) - .HasMaxLength(1024) - .HasField("_Homepage") - .UsePropertyAccessMode(PropertyAccessMode.Property); - - modelBuilder.Entity() - .HasMany(x => x.CustomItemMetadata) - .WithOne() - .HasForeignKey("CustomItemMetadata_CustomItemMetadata_Id") - .IsRequired(); - modelBuilder.Entity() - .HasMany(x => x.Releases) - .WithOne() - .HasForeignKey("Release_Releases_Id") - .IsRequired(); - - - modelBuilder.Entity() - .Property(t => t.EpisodeNumber) - .HasField("_EpisodeNumber") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .HasMany(x => x.Releases) - .WithOne() - .HasForeignKey("Release_Releases_Id") - .IsRequired(); - modelBuilder.Entity() - .HasMany(x => x.EpisodeMetadata) - .WithOne() - .HasForeignKey("EpisodeMetadata_EpisodeMetadata_Id") - .IsRequired(); - - modelBuilder.Entity() - .Property(t => t.Outline) - .HasMaxLength(1024) - .HasField("_Outline") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Plot) - .HasMaxLength(65535) - .HasField("_Plot") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Tagline) - .HasMaxLength(1024) - .HasField("_Tagline") - .UsePropertyAccessMode(PropertyAccessMode.Property); - - modelBuilder.Entity() - .ToTable("Genre") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .HasField("_Id") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.Name) - .HasMaxLength(255) - .IsRequired() - .HasField("_Name") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity().HasIndex(t => t.Name) - .IsUnique(); - modelBuilder.Entity() - .Property(t => t.Timestamp) - .IsRequired() - .HasField("_Timestamp") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .IsRowVersion(); - - modelBuilder.Entity() - .ToTable("Groups") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.Name) - .HasMaxLength(255) - .IsRequired(); - modelBuilder.Entity().Property("Timestamp").IsConcurrencyToken(); - modelBuilder.Entity() - .HasMany(x => x.GroupPermissions) - .WithOne() - .HasForeignKey("Permission_GroupPermissions_Id") - .IsRequired(); - modelBuilder.Entity() - .HasMany(x => x.ProviderMappings) - .WithOne() - .HasForeignKey("ProviderMapping_ProviderMappings_Id") - .IsRequired(); - modelBuilder.Entity() - .HasMany(x => x.Preferences) - .WithOne() - .HasForeignKey("Preference_Preferences_Id") - .IsRequired(); - - modelBuilder.Entity() - .ToTable("Library") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .HasField("_Id") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.Name) - .HasMaxLength(1024) - .IsRequired() - .HasField("_Name") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Timestamp) - .IsRequired() - .HasField("_Timestamp") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .IsRowVersion(); - - modelBuilder.Entity() - .ToTable("LibraryItem") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .HasField("_Id") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.UrlId) - .IsRequired() - .HasField("_UrlId") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity().HasIndex(t => t.UrlId) - .IsUnique(); - modelBuilder.Entity() - .Property(t => t.DateAdded) - .IsRequired() - .HasField("_DateAdded") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Timestamp) - .IsRequired() - .HasField("_Timestamp") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .IsRowVersion(); - modelBuilder.Entity() - .HasOne(x => x.LibraryRoot) - .WithOne() - .HasForeignKey("LibraryRoot_Id") - .IsRequired(); - - modelBuilder.Entity() - .ToTable("LibraryRoot") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .HasField("_Id") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.Path) - .HasMaxLength(65535) - .IsRequired() - .HasField("_Path") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.NetworkPath) - .HasMaxLength(65535) - .HasField("_NetworkPath") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Timestamp) - .IsRequired() - .HasField("_Timestamp") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .IsRowVersion(); - modelBuilder.Entity() - .HasOne(x => x.Library) - .WithOne() - .HasForeignKey("Library_Id") - .IsRequired(); - - modelBuilder.Entity() - .ToTable("MediaFile") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .HasField("_Id") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.Path) - .HasMaxLength(65535) - .IsRequired() - .HasField("_Path") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Kind) - .IsRequired() - .HasField("_Kind") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Timestamp) - .IsRequired() - .HasField("_Timestamp") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .IsRowVersion(); - modelBuilder.Entity() - .HasMany(x => x.MediaFileStreams) - .WithOne() - .HasForeignKey("MediaFileStream_MediaFileStreams_Id") - .IsRequired(); - - modelBuilder.Entity() - .ToTable("MediaFileStream") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .HasField("_Id") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.StreamNumber) - .IsRequired() - .HasField("_StreamNumber") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Timestamp) - .IsRequired() - .HasField("_Timestamp") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .IsRowVersion(); - - modelBuilder.Entity() - .ToTable("Metadata") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .HasField("_Id") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.Title) - .HasMaxLength(1024) - .IsRequired() - .HasField("_Title") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.OriginalTitle) - .HasMaxLength(1024) - .HasField("_OriginalTitle") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.SortTitle) - .HasMaxLength(1024) - .HasField("_SortTitle") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Language) - .HasMaxLength(3) - .IsRequired() - .HasField("_Language") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.ReleaseDate) - .HasField("_ReleaseDate") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.DateAdded) - .IsRequired() - .HasField("_DateAdded") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.DateModified) - .IsRequired() - .HasField("_DateModified") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Timestamp) - .IsRequired() - .HasField("_Timestamp") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .IsRowVersion(); - modelBuilder.Entity() - .HasMany(x => x.PersonRoles) - .WithOne() - .HasForeignKey("PersonRole_PersonRoles_Id") - .IsRequired(); - modelBuilder.Entity() - .HasMany(x => x.Genres) - .WithOne() - .HasForeignKey("Genre_Genres_Id") - .IsRequired(); - modelBuilder.Entity() - .HasMany(x => x.Artwork) - .WithOne() - .HasForeignKey("Artwork_Artwork_Id") - .IsRequired(); - modelBuilder.Entity() - .HasMany(x => x.Ratings) - .WithOne() - .HasForeignKey("Rating_Ratings_Id") - .IsRequired(); - modelBuilder.Entity() - .HasMany(x => x.Sources) - .WithOne() - .HasForeignKey("MetadataProviderId_Sources_Id") - .IsRequired(); - - modelBuilder.Entity() - .ToTable("MetadataProvider") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .HasField("_Id") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.Name) - .HasMaxLength(1024) - .IsRequired() - .HasField("_Name") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Timestamp) - .IsRequired() - .HasField("_Timestamp") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .IsRowVersion(); - - modelBuilder.Entity() - .ToTable("MetadataProviderId") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .HasField("_Id") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.ProviderId) - .HasMaxLength(255) - .IsRequired() - .HasField("_ProviderId") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Timestamp) - .IsRequired() - .HasField("_Timestamp") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .IsRowVersion(); - modelBuilder.Entity() - .HasOne(x => x.MetadataProvider) - .WithOne() - .HasForeignKey("MetadataProvider_Id") - .IsRequired(); - - modelBuilder.Entity() - .HasMany(x => x.Releases) - .WithOne() - .HasForeignKey("Release_Releases_Id") - .IsRequired(); - modelBuilder.Entity() - .HasMany(x => x.MovieMetadata) - .WithOne() - .HasForeignKey("MovieMetadata_MovieMetadata_Id") - .IsRequired(); - - modelBuilder.Entity() - .Property(t => t.Outline) - .HasMaxLength(1024) - .HasField("_Outline") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Plot) - .HasMaxLength(65535) - .HasField("_Plot") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Tagline) - .HasMaxLength(1024) - .HasField("_Tagline") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Country) - .HasMaxLength(2) - .HasField("_Country") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .HasMany(x => x.Studios) - .WithOne() - .HasForeignKey("Company_Studios_Id") - .IsRequired(); - - modelBuilder.Entity() - .HasMany(x => x.MusicAlbumMetadata) - .WithOne() - .HasForeignKey("MusicAlbumMetadata_MusicAlbumMetadata_Id") - .IsRequired(); - modelBuilder.Entity() - .HasMany(x => x.Tracks) - .WithOne() - .HasForeignKey("Track_Tracks_Id") - .IsRequired(); - - modelBuilder.Entity() - .Property(t => t.Barcode) - .HasMaxLength(255) - .HasField("_Barcode") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.LabelNumber) - .HasMaxLength(255) - .HasField("_LabelNumber") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Country) - .HasMaxLength(2) - .HasField("_Country") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .HasMany(x => x.Labels) - .WithOne() - .HasForeignKey("Company_Labels_Id") - .IsRequired(); - - modelBuilder.Entity() - .ToTable("Permissions") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.Kind) - .IsRequired() - .HasField("_Kind") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Value) - .IsRequired(); - modelBuilder.Entity().Property("Timestamp").IsConcurrencyToken(); - - modelBuilder.Entity() - .ToTable("Person") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .HasField("_Id") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.UrlId) - .IsRequired() - .HasField("_UrlId") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Name) - .HasMaxLength(1024) - .IsRequired() - .HasField("_Name") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.SourceId) - .HasMaxLength(255) - .HasField("_SourceId") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.DateAdded) - .IsRequired() - .HasField("_DateAdded") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.DateModified) - .IsRequired() - .HasField("_DateModified") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Timestamp) - .IsRequired() - .HasField("_Timestamp") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .IsRowVersion(); - modelBuilder.Entity() - .HasMany(x => x.Sources) - .WithOne() - .HasForeignKey("MetadataProviderId_Sources_Id") - .IsRequired(); - - modelBuilder.Entity() - .ToTable("PersonRole") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .HasField("_Id") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.Role) - .HasMaxLength(1024) - .HasField("_Role") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Type) - .IsRequired() - .HasField("_Type") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Timestamp) - .IsRequired() - .HasField("_Timestamp") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .IsRowVersion(); - modelBuilder.Entity() - .HasOne(x => x.Person) - .WithOne() - .HasForeignKey("Person_Id") - .IsRequired() - .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity() - .HasOne(x => x.Artwork) - .WithOne() - .HasForeignKey("Artwork_Artwork_Id") - .IsRequired(); - modelBuilder.Entity() - .HasMany(x => x.Sources) - .WithOne() - .HasForeignKey("MetadataProviderId_Sources_Id") - .IsRequired(); - - modelBuilder.Entity() - .HasMany(x => x.PhotoMetadata) - .WithOne() - .HasForeignKey("PhotoMetadata_PhotoMetadata_Id") - .IsRequired(); - modelBuilder.Entity() - .HasMany(x => x.Releases) - .WithOne() - .HasForeignKey("Release_Releases_Id") - .IsRequired(); - - - modelBuilder.Entity() - .ToTable("Preferences") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.Kind) - .IsRequired(); - modelBuilder.Entity() - .Property(t => t.Value) - .HasMaxLength(65535) - .IsRequired(); - modelBuilder.Entity().Property("Timestamp").IsConcurrencyToken(); - - modelBuilder.Entity() - .ToTable("ProviderMappings") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.ProviderName) - .HasMaxLength(255) - .IsRequired(); - modelBuilder.Entity() - .Property(t => t.ProviderSecrets) - .HasMaxLength(65535) - .IsRequired(); - modelBuilder.Entity() - .Property(t => t.ProviderData) - .HasMaxLength(65535) - .IsRequired(); - modelBuilder.Entity().Property("Timestamp").IsConcurrencyToken(); - - modelBuilder.Entity() - .ToTable("Rating") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .HasField("_Id") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.Value) - .IsRequired() - .HasField("_Value") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Votes) - .HasField("_Votes") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Timestamp) - .IsRequired() - .HasField("_Timestamp") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .IsRowVersion(); - modelBuilder.Entity() - .HasOne(x => x.RatingType) - .WithOne() - .HasForeignKey("RatingSource_RatingType_Id") - .IsRequired(); - - modelBuilder.Entity() - .ToTable("RatingType") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .HasField("_Id") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.Name) - .HasMaxLength(1024) - .HasField("_Name") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.MaximumValue) - .IsRequired() - .HasField("_MaximumValue") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.MinimumValue) - .IsRequired() - .HasField("_MinimumValue") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Timestamp) - .IsRequired() - .HasField("_Timestamp") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .IsRowVersion(); - modelBuilder.Entity() - .HasOne(x => x.Source) - .WithOne() - .HasForeignKey("MetadataProviderId_Source_Id") - .IsRequired(); - - modelBuilder.Entity() - .ToTable("Release") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .HasField("_Id") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.Name) - .HasMaxLength(1024) - .IsRequired() - .HasField("_Name") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Timestamp) - .IsRequired() - .HasField("_Timestamp") - .UsePropertyAccessMode(PropertyAccessMode.Property) - .IsRowVersion(); - modelBuilder.Entity() - .HasMany(x => x.MediaFiles) - .WithOne() - .HasForeignKey("MediaFile_MediaFiles_Id") - .IsRequired(); - modelBuilder.Entity() - .HasMany(x => x.Chapters) - .WithOne() - .HasForeignKey("Chapter_Chapters_Id") - .IsRequired(); - - modelBuilder.Entity() - .Property(t => t.SeasonNumber) - .HasField("_SeasonNumber") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .HasMany(x => x.SeasonMetadata) - .WithOne() - .HasForeignKey("SeasonMetadata_SeasonMetadata_Id") - .IsRequired(); - modelBuilder.Entity() - .HasMany(x => x.Episodes) - .WithOne() - .HasForeignKey("Episode_Episodes_Id") - .IsRequired(); - - modelBuilder.Entity() - .Property(t => t.Outline) - .HasMaxLength(1024) - .HasField("_Outline") - .UsePropertyAccessMode(PropertyAccessMode.Property); - - modelBuilder.Entity() - .Property(t => t.AirsDayOfWeek) - .HasField("_AirsDayOfWeek") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.AirsTime) - .HasField("_AirsTime") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.FirstAired) - .HasField("_FirstAired") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .HasMany(x => x.SeriesMetadata) - .WithOne() - .HasForeignKey("SeriesMetadata_SeriesMetadata_Id") - .IsRequired(); - modelBuilder.Entity() - .HasMany(x => x.Seasons) - .WithOne() - .HasForeignKey("Season_Seasons_Id") - .IsRequired(); - - modelBuilder.Entity() - .Property(t => t.Outline) - .HasMaxLength(1024) - .HasField("_Outline") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Plot) - .HasMaxLength(65535) - .HasField("_Plot") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Tagline) - .HasMaxLength(1024) - .HasField("_Tagline") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .Property(t => t.Country) - .HasMaxLength(2) - .HasField("_Country") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .HasMany(x => x.Networks) - .WithOne() - .HasForeignKey("Company_Networks_Id") - .IsRequired(); - - modelBuilder.Entity() - .Property(t => t.TrackNumber) - .HasField("_TrackNumber") - .UsePropertyAccessMode(PropertyAccessMode.Property); - modelBuilder.Entity() - .HasMany(x => x.Releases) - .WithOne() - .HasForeignKey("Release_Releases_Id") - .IsRequired(); - modelBuilder.Entity() - .HasMany(x => x.TrackMetadata) - .WithOne() - .HasForeignKey("TrackMetadata_TrackMetadata_Id") - .IsRequired(); - - - modelBuilder.Entity() - .ToTable("Users") - .HasKey(t => t.Id); - modelBuilder.Entity() - .Property(t => t.Id) - .IsRequired() - .ValueGeneratedOnAdd(); - modelBuilder.Entity() - .Property(t => t.LastLoginTimestamp) - .IsRequired() - .IsRowVersion(); - modelBuilder.Entity() - .Property(t => t.Username) - .HasMaxLength(255) - .IsRequired(); - modelBuilder.Entity() - .Property(t => t.Password) - .HasMaxLength(65535); - modelBuilder.Entity() - .Property(t => t.MustUpdatePassword) - .IsRequired(); - modelBuilder.Entity() - .Property(t => t.AudioLanguagePreference) - .HasMaxLength(255) - .IsRequired(); - modelBuilder.Entity() - .Property(t => t.AuthenticationProviderId) - .HasMaxLength(255) - .IsRequired(); - modelBuilder.Entity() - .Property(t => t.GroupedFolders) - .HasMaxLength(65535); - modelBuilder.Entity() - .Property(t => t.InvalidLoginAttemptCount) - .IsRequired(); - modelBuilder.Entity() - .Property(t => t.LatestItemExcludes) - .HasMaxLength(65535); - modelBuilder.Entity() - .Property(t => t.MyMediaExcludes) - .HasMaxLength(65535); - modelBuilder.Entity() - .Property(t => t.OrderedViews) - .HasMaxLength(65535); - modelBuilder.Entity() - .Property(t => t.SubtitleMode) - .HasMaxLength(255) - .IsRequired(); - modelBuilder.Entity() - .Property(t => t.PlayDefaultAudioTrack) - .IsRequired(); - modelBuilder.Entity() - .Property(t => t.SubtitleLanguagePrefernce) - .HasMaxLength(255); - modelBuilder.Entity() - .HasMany(x => x.Groups) - .WithOne() - .HasForeignKey("Group_Groups_Id") - .IsRequired(); - modelBuilder.Entity() - .HasMany(x => x.Permissions) - .WithOne() - .HasForeignKey("Permission_Permissions_Id") - .IsRequired(); - modelBuilder.Entity() - .HasMany(x => x.ProviderMappings) - .WithOne() - .HasForeignKey("ProviderMapping_ProviderMappings_Id") - .IsRequired(); - modelBuilder.Entity() - .HasMany(x => x.Preferences) - .WithOne() - .HasForeignKey("Preference_Preferences_Id") - .IsRequired(); - - OnModelCreatedImpl(modelBuilder); - } - } -} diff --git a/Jellyfin.Data/Entities/ActivityLog.cs b/Jellyfin.Data/Entities/ActivityLog.cs new file mode 100644 index 000000000..633838991 --- /dev/null +++ b/Jellyfin.Data/Entities/ActivityLog.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Jellyfin.Data.Entities +{ + [Table("ActivityLog")] + public partial class ActivityLog + { + partial void Init(); + + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected ActivityLog() + { + Init(); + } + + /// + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// + public static ActivityLog CreateActivityLogUnsafe() + { + return new ActivityLog(); + } + + /// + /// Public constructor with required data + /// + /// + /// + /// + /// + /// + public ActivityLog(string name, string type, Guid userid, DateTime datecreated, Microsoft.Extensions.Logging.LogLevel logseverity) + { + if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); + this.Name = name; + + if (string.IsNullOrEmpty(type)) throw new ArgumentNullException(nameof(type)); + this.Type = type; + + this.UserId = userid; + + this.DateCreated = datecreated; + + this.LogSeverity = logseverity; + + + Init(); + } + + /// + /// Static create function (for use in LINQ queries, etc.) + /// + /// + /// + /// + /// + /// + public static ActivityLog Create(string name, string type, Guid userid, DateTime datecreated, Microsoft.Extensions.Logging.LogLevel logseverity) + { + return new ActivityLog(name, type, userid, datecreated, logseverity); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// + /// Identity, Indexed, Required + /// + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// + /// Required, Max length = 512 + /// + [Required] + [MaxLength(512)] + [StringLength(512)] + public string Name { get; set; } + + /// + /// Max length = 512 + /// + [MaxLength(512)] + [StringLength(512)] + public string Overview { get; set; } + + /// + /// Max length = 512 + /// + [MaxLength(512)] + [StringLength(512)] + public string ShortOverview { get; set; } + + /// + /// Required, Max length = 256 + /// + [Required] + [MaxLength(256)] + [StringLength(256)] + public string Type { get; set; } + + /// + /// Required + /// + [Required] + public Guid UserId { get; set; } + + /// + /// Max length = 256 + /// + [MaxLength(256)] + [StringLength(256)] + public string ItemId { get; set; } + + /// + /// Required + /// + [Required] + public DateTime DateCreated { get; set; } + + /// + /// Required + /// + [Required] + public Microsoft.Extensions.Logging.LogLevel LogSeverity { get; set; } + + /// + /// Required, ConcurrenyToken + /// + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + } +} + diff --git a/Jellyfin.Data/ISavingChanges.cs b/Jellyfin.Data/ISavingChanges.cs new file mode 100644 index 000000000..5388b921d --- /dev/null +++ b/Jellyfin.Data/ISavingChanges.cs @@ -0,0 +1,9 @@ +#pragma warning disable CS1591 + +namespace Jellyfin.Data +{ + public interface ISavingChanges + { + void OnSavingChanges(); + } +} diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 73ea593b0..8eae366ba 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -1,12 +1,30 @@ - netstandard2.0 + netstandard2.0;netstandard2.1 + false + true + + ../jellyfin.ruleset + + + + + + + + + + - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs new file mode 100644 index 000000000..d7bbf793c --- /dev/null +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.Querying; + +namespace Jellyfin.Server.Implementations.Activity +{ + /// + /// Manages the storage and retrieval of instances. + /// + public class ActivityManager : IActivityManager + { + private JellyfinDbProvider _provider; + + /// + /// Initializes a new instance of the class. + /// + /// The Jellyfin database provider. + public ActivityManager(JellyfinDbProvider provider) + { + _provider = provider; + } + + /// + public event EventHandler> EntryCreated; + + /// + public void Create(ActivityLog entry) + { + using var dbContext = _provider.CreateContext(); + dbContext.ActivityLogs.Add(entry); + dbContext.SaveChanges(); + + EntryCreated?.Invoke(this, new GenericEventArgs(ConvertToOldModel(entry))); + } + + /// + public async Task CreateAsync(ActivityLog entry) + { + using var dbContext = _provider.CreateContext(); + await dbContext.ActivityLogs.AddAsync(entry); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + + EntryCreated?.Invoke(this, new GenericEventArgs(ConvertToOldModel(entry))); + } + + /// + public QueryResult GetPagedResult( + Func, IEnumerable> func, + int? startIndex, + int? limit) + { + using var dbContext = _provider.CreateContext(); + + var result = func.Invoke(dbContext.ActivityLogs).AsQueryable(); + + if (startIndex.HasValue) + { + result = result.Where(entry => entry.Id >= startIndex.Value); + } + + if (limit.HasValue) + { + result = result.OrderByDescending(entry => entry.DateCreated).Take(limit.Value); + } + + // This converts the objects from the new database model to the old for compatibility with the existing API. + var list = result.Select(entry => ConvertToOldModel(entry)).ToList(); + + return new QueryResult() + { + Items = list, + TotalRecordCount = list.Count + }; + } + + /// + public QueryResult GetPagedResult(int? startIndex, int? limit) + { + return GetPagedResult(logs => logs, startIndex, limit); + } + + private static ActivityLogEntry ConvertToOldModel(ActivityLog entry) + { + return new ActivityLogEntry + { + Id = entry.Id, + Name = entry.Name, + Overview = entry.Overview, + ShortOverview = entry.ShortOverview, + Type = entry.Type, + ItemId = entry.ItemId, + UserId = entry.UserId, + Date = entry.DateCreated, + Severity = entry.LogSeverity + }; + } + } +} diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj new file mode 100644 index 000000000..a31f28f64 --- /dev/null +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -0,0 +1,34 @@ + + + + netcoreapp3.1 + false + true + true + + + + ../jellyfin.ruleset + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs new file mode 100644 index 000000000..9c1a23877 --- /dev/null +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -0,0 +1,119 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1201 // Constuctors should not follow properties +#pragma warning disable SA1516 // Elements should be followed by a blank line +#pragma warning disable SA1623 // Property's documentation should begin with gets or sets +#pragma warning disable SA1629 // Documentation should end with a period +#pragma warning disable SA1648 // Inheritdoc should be used with inheriting class + +using System.Linq; +using Jellyfin.Data; +using Jellyfin.Data.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Jellyfin.Server.Implementations +{ + /// + public partial class JellyfinDb : DbContext + { + public virtual DbSet ActivityLogs { get; set; } + public virtual DbSet Artwork { get; set; } + public virtual DbSet Books { get; set; } + public virtual DbSet BookMetadata { get; set; } + public virtual DbSet Chapters { get; set; } + public virtual DbSet Collections { get; set; } + public virtual DbSet CollectionItems { get; set; } + public virtual DbSet Companies { get; set; } + public virtual DbSet CompanyMetadata { get; set; } + public virtual DbSet CustomItems { get; set; } + public virtual DbSet CustomItemMetadata { get; set; } + public virtual DbSet Episodes { get; set; } + public virtual DbSet EpisodeMetadata { get; set; } + public virtual DbSet Genres { get; set; } + public virtual DbSet Groups { get; set; } + public virtual DbSet Libraries { get; set; } + public virtual DbSet LibraryItems { get; set; } + public virtual DbSet LibraryRoot { get; set; } + public virtual DbSet MediaFiles { get; set; } + public virtual DbSet MediaFileStream { get; set; } + public virtual DbSet Metadata { get; set; } + public virtual DbSet MetadataProviders { get; set; } + public virtual DbSet MetadataProviderIds { get; set; } + public virtual DbSet Movies { get; set; } + public virtual DbSet MovieMetadata { get; set; } + public virtual DbSet MusicAlbums { get; set; } + public virtual DbSet MusicAlbumMetadata { get; set; } + public virtual DbSet Permissions { get; set; } + public virtual DbSet People { get; set; } + public virtual DbSet PersonRoles { get; set; } + public virtual DbSet Photo { get; set; } + public virtual DbSet PhotoMetadata { get; set; } + public virtual DbSet Preferences { get; set; } + public virtual DbSet ProviderMappings { get; set; } + public virtual DbSet Ratings { get; set; } + + /// + /// Repository for global::Jellyfin.Data.Entities.RatingSource - This is the entity to + /// store review ratings, not age ratings + /// + public virtual DbSet RatingSources { get; set; } + public virtual DbSet Releases { get; set; } + public virtual DbSet Seasons { get; set; } + public virtual DbSet SeasonMetadata { get; set; } + public virtual DbSet Series { get; set; } + public virtual DbSet SeriesMetadata { get; set; } + public virtual DbSet Tracks { get; set; } + public virtual DbSet TrackMetadata { get; set; } + public virtual DbSet Users { get; set; } + + /// + /// Gets or sets the default connection string. + /// + public static string ConnectionString { get; set; } = @"Data Source=jellyfin.db"; + + /// + public JellyfinDb(DbContextOptions options) : base(options) + { + } + + partial void CustomInit(DbContextOptionsBuilder optionsBuilder); + + /// + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + CustomInit(optionsBuilder); + } + + partial void OnModelCreatingImpl(ModelBuilder modelBuilder); + partial void OnModelCreatedImpl(ModelBuilder modelBuilder); + + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + OnModelCreatingImpl(modelBuilder); + + modelBuilder.HasDefaultSchema("jellyfin"); + + modelBuilder.Entity().HasIndex(t => t.Kind); + + modelBuilder.Entity().HasIndex(t => t.Name) + .IsUnique(); + + modelBuilder.Entity().HasIndex(t => t.UrlId) + .IsUnique(); + + OnModelCreatedImpl(modelBuilder); + } + + public override int SaveChanges() + { + foreach (var entity in ChangeTracker.Entries().Where(e => e.State == EntityState.Modified)) + { + var saveEntity = entity.Entity as ISavingChanges; + saveEntity.OnSavingChanges(); + } + + return base.SaveChanges(); + } + } +} diff --git a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs new file mode 100644 index 000000000..8fdeab088 --- /dev/null +++ b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Jellyfin.Server.Implementations +{ + /// + /// Factory class for generating new instances. + /// + public class JellyfinDbProvider + { + private readonly IServiceProvider _serviceProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The application's service provider. + public JellyfinDbProvider(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + serviceProvider.GetService().Database.Migrate(); + } + + /// + /// Creates a new context. + /// + /// The newly created context. + public JellyfinDb CreateContext() + { + return _serviceProvider.GetService(); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20200430215054_InitialSchema.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200430215054_InitialSchema.Designer.cs new file mode 100644 index 000000000..3fb0fd51e --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20200430215054_InitialSchema.Designer.cs @@ -0,0 +1,1513 @@ +#pragma warning disable CS1591 + +// +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDb))] + [Migration("20200430215054_InitialSchema")] + partial class InitialSchema + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("jellyfin") + .HasAnnotation("ProductVersion", "3.1.3"); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("Overview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ActivityLog"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Artwork", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("PersonRole_PersonRoles_Id") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Kind"); + + b.HasIndex("PersonRole_PersonRoles_Id"); + + b.ToTable("Artwork"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Chapter_Chapters_Id") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(3); + + b.Property("Name") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("TimeEnd") + .HasColumnType("INTEGER"); + + b.Property("TimeStart") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Chapter_Chapters_Id"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Collection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Collection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CollectionItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CollectionItem_CollectionItem_Id") + .HasColumnType("INTEGER"); + + b.Property("CollectionItem_Next_Id") + .HasColumnType("INTEGER"); + + b.Property("CollectionItem_Previous_Id") + .HasColumnType("INTEGER"); + + b.Property("LibraryItem_Id") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionItem_CollectionItem_Id"); + + b.HasIndex("CollectionItem_Next_Id"); + + b.HasIndex("CollectionItem_Previous_Id"); + + b.HasIndex("LibraryItem_Id"); + + b.ToTable("CollectionItem"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Company_Labels_Id") + .HasColumnType("INTEGER"); + + b.Property("Company_Networks_Id") + .HasColumnType("INTEGER"); + + b.Property("Company_Parent_Id") + .HasColumnType("INTEGER"); + + b.Property("Company_Publishers_Id") + .HasColumnType("INTEGER"); + + b.Property("Company_Studios_Id") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Company_Labels_Id"); + + b.HasIndex("Company_Networks_Id"); + + b.HasIndex("Company_Parent_Id"); + + b.HasIndex("Company_Publishers_Id"); + + b.HasIndex("Company_Studios_Id"); + + b.ToTable("Company"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("PersonRole_PersonRoles_Id") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("PersonRole_PersonRoles_Id"); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Group_Groups_Id") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Group_Groups_Id"); + + b.ToTable("Group"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.LibraryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LibraryRoot_Id") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UrlId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryRoot_Id"); + + b.HasIndex("UrlId") + .IsUnique(); + + b.ToTable("LibraryItem"); + + b.HasDiscriminator("Discriminator").HasValue("LibraryItem"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.LibraryRoot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Library_Id") + .HasColumnType("INTEGER"); + + b.Property("NetworkPath") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Library_Id"); + + b.ToTable("LibraryRoot"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("MediaFile_MediaFiles_Id") + .HasColumnType("INTEGER"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MediaFile_MediaFiles_Id"); + + b.ToTable("MediaFile"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaFileStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MediaFileStream_MediaFileStreams_Id") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("StreamNumber") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MediaFileStream_MediaFileStreams_Id"); + + b.ToTable("MediaFileStream"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Metadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(3); + + b.Property("OriginalTitle") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SortTitle") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.HasKey("Id"); + + b.ToTable("Metadata"); + + b.HasDiscriminator("Discriminator").HasValue("Metadata"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MetadataProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MetadataProvider"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MetadataProviderId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MetadataProviderId_Sources_Id") + .HasColumnType("INTEGER"); + + b.Property("MetadataProvider_Id") + .HasColumnType("INTEGER"); + + b.Property("PersonRole_PersonRoles_Id") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MetadataProviderId_Sources_Id"); + + b.HasIndex("MetadataProvider_Id"); + + b.HasIndex("PersonRole_PersonRoles_Id"); + + b.ToTable("MetadataProviderId"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_GroupPermissions_Id") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Id") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Permission_GroupPermissions_Id"); + + b.HasIndex("Permission_Permissions_Id"); + + b.ToTable("Permission"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SourceId") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UrlId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PersonRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Artwork_Artwork_Id") + .HasColumnType("INTEGER"); + + b.Property("PersonRole_PersonRoles_Id") + .HasColumnType("INTEGER"); + + b.Property("Person_Id") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Artwork_Artwork_Id"); + + b.HasIndex("PersonRole_PersonRoles_Id"); + + b.HasIndex("Person_Id"); + + b.ToTable("PersonRole"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Id") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.HasKey("Id"); + + b.HasIndex("Preference_Preferences_Id"); + + b.ToTable("Preference"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ProviderMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ProviderData") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("ProviderMapping_ProviderMappings_Id") + .HasColumnType("INTEGER"); + + b.Property("ProviderName") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("ProviderSecrets") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderMapping_ProviderMappings_Id"); + + b.ToTable("ProviderMapping"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Rating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("PersonRole_PersonRoles_Id") + .HasColumnType("INTEGER"); + + b.Property("RatingSource_RatingType_Id") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("REAL"); + + b.Property("Votes") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PersonRole_PersonRoles_Id"); + + b.HasIndex("RatingSource_RatingType_Id"); + + b.ToTable("Rating"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.RatingSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MaximumValue") + .HasColumnType("REAL"); + + b.Property("MetadataProviderId_Source_Id") + .HasColumnType("INTEGER"); + + b.Property("MinimumValue") + .HasColumnType("REAL"); + + b.Property("Name") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MetadataProviderId_Source_Id"); + + b.ToTable("RatingSource"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Release", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("Release_Releases_Id") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Release_Releases_Id"); + + b.ToTable("Release"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudioLanguagePreference") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("GroupedFolders") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LatestItemExcludes") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("MyMediaExcludes") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("OrderedViews") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("Password") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePrefernce") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("SubtitleMode") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Book", b => + { + b.HasBaseType("Jellyfin.Data.Entities.LibraryItem"); + + b.ToTable("Book"); + + b.HasDiscriminator().HasValue("Book"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItem", b => + { + b.HasBaseType("Jellyfin.Data.Entities.LibraryItem"); + + b.ToTable("LibraryItem"); + + b.HasDiscriminator().HasValue("CustomItem"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Episode", b => + { + b.HasBaseType("Jellyfin.Data.Entities.LibraryItem"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("Episode_Episodes_Id") + .HasColumnType("INTEGER"); + + b.HasIndex("Episode_Episodes_Id"); + + b.ToTable("Episode"); + + b.HasDiscriminator().HasValue("Episode"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Movie", b => + { + b.HasBaseType("Jellyfin.Data.Entities.LibraryItem"); + + b.ToTable("Movie"); + + b.HasDiscriminator().HasValue("Movie"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MusicAlbum", b => + { + b.HasBaseType("Jellyfin.Data.Entities.LibraryItem"); + + b.ToTable("MusicAlbum"); + + b.HasDiscriminator().HasValue("MusicAlbum"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Photo", b => + { + b.HasBaseType("Jellyfin.Data.Entities.LibraryItem"); + + b.ToTable("Photo"); + + b.HasDiscriminator().HasValue("Photo"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Season", b => + { + b.HasBaseType("Jellyfin.Data.Entities.LibraryItem"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("Season_Seasons_Id") + .HasColumnType("INTEGER"); + + b.HasIndex("Season_Seasons_Id"); + + b.ToTable("Season"); + + b.HasDiscriminator().HasValue("Season"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Series", b => + { + b.HasBaseType("Jellyfin.Data.Entities.LibraryItem"); + + b.Property("AirsDayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("AirsTime") + .HasColumnType("TEXT"); + + b.Property("FirstAired") + .HasColumnType("TEXT"); + + b.ToTable("Series"); + + b.HasDiscriminator().HasValue("Series"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Track", b => + { + b.HasBaseType("Jellyfin.Data.Entities.LibraryItem"); + + b.Property("TrackNumber") + .HasColumnType("INTEGER"); + + b.Property("Track_Tracks_Id") + .HasColumnType("INTEGER"); + + b.HasIndex("Track_Tracks_Id"); + + b.ToTable("Track"); + + b.HasDiscriminator().HasValue("Track"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BookMetadata", b => + { + b.HasBaseType("Jellyfin.Data.Entities.Metadata"); + + b.Property("BookMetadata_BookMetadata_Id") + .HasColumnType("INTEGER"); + + b.Property("ISBN") + .HasColumnType("INTEGER"); + + b.HasIndex("BookMetadata_BookMetadata_Id"); + + b.ToTable("Metadata"); + + b.HasDiscriminator().HasValue("BookMetadata"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CompanyMetadata", b => + { + b.HasBaseType("Jellyfin.Data.Entities.Metadata"); + + b.Property("CompanyMetadata_CompanyMetadata_Id") + .HasColumnType("INTEGER"); + + b.Property("Country") + .HasColumnType("TEXT") + .HasMaxLength(2); + + b.Property("Description") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("Headquarters") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Homepage") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.HasIndex("CompanyMetadata_CompanyMetadata_Id"); + + b.ToTable("CompanyMetadata"); + + b.HasDiscriminator().HasValue("CompanyMetadata"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemMetadata", b => + { + b.HasBaseType("Jellyfin.Data.Entities.Metadata"); + + b.Property("CustomItemMetadata_CustomItemMetadata_Id") + .HasColumnType("INTEGER"); + + b.HasIndex("CustomItemMetadata_CustomItemMetadata_Id"); + + b.ToTable("CustomItemMetadata"); + + b.HasDiscriminator().HasValue("CustomItemMetadata"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.EpisodeMetadata", b => + { + b.HasBaseType("Jellyfin.Data.Entities.Metadata"); + + b.Property("EpisodeMetadata_EpisodeMetadata_Id") + .HasColumnType("INTEGER"); + + b.Property("Outline") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("Plot") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("Tagline") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.HasIndex("EpisodeMetadata_EpisodeMetadata_Id"); + + b.ToTable("EpisodeMetadata"); + + b.HasDiscriminator().HasValue("EpisodeMetadata"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MovieMetadata", b => + { + b.HasBaseType("Jellyfin.Data.Entities.Metadata"); + + b.Property("Country") + .HasColumnName("MovieMetadata_Country") + .HasColumnType("TEXT") + .HasMaxLength(2); + + b.Property("MovieMetadata_MovieMetadata_Id") + .HasColumnType("INTEGER"); + + b.Property("Outline") + .HasColumnName("MovieMetadata_Outline") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("Plot") + .HasColumnName("MovieMetadata_Plot") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("Tagline") + .HasColumnName("MovieMetadata_Tagline") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.HasIndex("MovieMetadata_MovieMetadata_Id"); + + b.ToTable("MovieMetadata"); + + b.HasDiscriminator().HasValue("MovieMetadata"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MusicAlbumMetadata", b => + { + b.HasBaseType("Jellyfin.Data.Entities.Metadata"); + + b.Property("Barcode") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Country") + .HasColumnName("MusicAlbumMetadata_Country") + .HasColumnType("TEXT") + .HasMaxLength(2); + + b.Property("LabelNumber") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("MusicAlbumMetadata_MusicAlbumMetadata_Id") + .HasColumnType("INTEGER"); + + b.HasIndex("MusicAlbumMetadata_MusicAlbumMetadata_Id"); + + b.ToTable("MusicAlbumMetadata"); + + b.HasDiscriminator().HasValue("MusicAlbumMetadata"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PhotoMetadata", b => + { + b.HasBaseType("Jellyfin.Data.Entities.Metadata"); + + b.Property("PhotoMetadata_PhotoMetadata_Id") + .HasColumnType("INTEGER"); + + b.HasIndex("PhotoMetadata_PhotoMetadata_Id"); + + b.ToTable("PhotoMetadata"); + + b.HasDiscriminator().HasValue("PhotoMetadata"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.SeasonMetadata", b => + { + b.HasBaseType("Jellyfin.Data.Entities.Metadata"); + + b.Property("Outline") + .HasColumnName("SeasonMetadata_Outline") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("SeasonMetadata_SeasonMetadata_Id") + .HasColumnType("INTEGER"); + + b.HasIndex("SeasonMetadata_SeasonMetadata_Id"); + + b.ToTable("SeasonMetadata"); + + b.HasDiscriminator().HasValue("SeasonMetadata"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.SeriesMetadata", b => + { + b.HasBaseType("Jellyfin.Data.Entities.Metadata"); + + b.Property("Country") + .HasColumnName("SeriesMetadata_Country") + .HasColumnType("TEXT") + .HasMaxLength(2); + + b.Property("Outline") + .HasColumnName("SeriesMetadata_Outline") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("Plot") + .HasColumnName("SeriesMetadata_Plot") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("SeriesMetadata_SeriesMetadata_Id") + .HasColumnType("INTEGER"); + + b.Property("Tagline") + .HasColumnName("SeriesMetadata_Tagline") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.HasIndex("SeriesMetadata_SeriesMetadata_Id"); + + b.ToTable("SeriesMetadata"); + + b.HasDiscriminator().HasValue("SeriesMetadata"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.TrackMetadata", b => + { + b.HasBaseType("Jellyfin.Data.Entities.Metadata"); + + b.Property("TrackMetadata_TrackMetadata_Id") + .HasColumnType("INTEGER"); + + b.HasIndex("TrackMetadata_TrackMetadata_Id"); + + b.ToTable("TrackMetadata"); + + b.HasDiscriminator().HasValue("TrackMetadata"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Artwork", b => + { + b.HasOne("Jellyfin.Data.Entities.Metadata", null) + .WithMany("Artwork") + .HasForeignKey("PersonRole_PersonRoles_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.HasOne("Jellyfin.Data.Entities.Release", null) + .WithMany("Chapters") + .HasForeignKey("Chapter_Chapters_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CollectionItem", b => + { + b.HasOne("Jellyfin.Data.Entities.Collection", null) + .WithMany("CollectionItem") + .HasForeignKey("CollectionItem_CollectionItem_Id"); + + b.HasOne("Jellyfin.Data.Entities.CollectionItem", "Next") + .WithMany() + .HasForeignKey("CollectionItem_Next_Id"); + + b.HasOne("Jellyfin.Data.Entities.CollectionItem", "Previous") + .WithMany() + .HasForeignKey("CollectionItem_Previous_Id"); + + b.HasOne("Jellyfin.Data.Entities.LibraryItem", "LibraryItem") + .WithMany() + .HasForeignKey("LibraryItem_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Company", b => + { + b.HasOne("Jellyfin.Data.Entities.MusicAlbumMetadata", null) + .WithMany("Labels") + .HasForeignKey("Company_Labels_Id"); + + b.HasOne("Jellyfin.Data.Entities.SeriesMetadata", null) + .WithMany("Networks") + .HasForeignKey("Company_Networks_Id"); + + b.HasOne("Jellyfin.Data.Entities.Company", "Parent") + .WithMany() + .HasForeignKey("Company_Parent_Id"); + + b.HasOne("Jellyfin.Data.Entities.BookMetadata", null) + .WithMany("Publishers") + .HasForeignKey("Company_Publishers_Id"); + + b.HasOne("Jellyfin.Data.Entities.MovieMetadata", null) + .WithMany("Studios") + .HasForeignKey("Company_Studios_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Genre", b => + { + b.HasOne("Jellyfin.Data.Entities.Metadata", null) + .WithMany("Genres") + .HasForeignKey("PersonRole_PersonRoles_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Group", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Groups") + .HasForeignKey("Group_Groups_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.LibraryItem", b => + { + b.HasOne("Jellyfin.Data.Entities.LibraryRoot", "LibraryRoot") + .WithMany() + .HasForeignKey("LibraryRoot_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.LibraryRoot", b => + { + b.HasOne("Jellyfin.Data.Entities.Library", "Library") + .WithMany() + .HasForeignKey("Library_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaFile", b => + { + b.HasOne("Jellyfin.Data.Entities.Release", null) + .WithMany("MediaFiles") + .HasForeignKey("MediaFile_MediaFiles_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaFileStream", b => + { + b.HasOne("Jellyfin.Data.Entities.MediaFile", null) + .WithMany("MediaFileStreams") + .HasForeignKey("MediaFileStream_MediaFileStreams_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MetadataProviderId", b => + { + b.HasOne("Jellyfin.Data.Entities.Person", null) + .WithMany("Sources") + .HasForeignKey("MetadataProviderId_Sources_Id"); + + b.HasOne("Jellyfin.Data.Entities.PersonRole", null) + .WithMany("Sources") + .HasForeignKey("MetadataProviderId_Sources_Id"); + + b.HasOne("Jellyfin.Data.Entities.MetadataProvider", "MetadataProvider") + .WithMany() + .HasForeignKey("MetadataProvider_Id"); + + b.HasOne("Jellyfin.Data.Entities.Metadata", null) + .WithMany("Sources") + .HasForeignKey("PersonRole_PersonRoles_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.Group", null) + .WithMany("GroupPermissions") + .HasForeignKey("Permission_GroupPermissions_Id"); + + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("Permission_Permissions_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PersonRole", b => + { + b.HasOne("Jellyfin.Data.Entities.Artwork", "Artwork") + .WithMany() + .HasForeignKey("Artwork_Artwork_Id"); + + b.HasOne("Jellyfin.Data.Entities.Metadata", null) + .WithMany("PersonRoles") + .HasForeignKey("PersonRole_PersonRoles_Id"); + + b.HasOne("Jellyfin.Data.Entities.Person", "Person") + .WithMany() + .HasForeignKey("Person_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.Group", null) + .WithMany("Preferences") + .HasForeignKey("Preference_Preferences_Id"); + + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("Preference_Preferences_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ProviderMapping", b => + { + b.HasOne("Jellyfin.Data.Entities.Group", null) + .WithMany("ProviderMappings") + .HasForeignKey("ProviderMapping_ProviderMappings_Id"); + + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ProviderMappings") + .HasForeignKey("ProviderMapping_ProviderMappings_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Rating", b => + { + b.HasOne("Jellyfin.Data.Entities.Metadata", null) + .WithMany("Ratings") + .HasForeignKey("PersonRole_PersonRoles_Id"); + + b.HasOne("Jellyfin.Data.Entities.RatingSource", "RatingType") + .WithMany() + .HasForeignKey("RatingSource_RatingType_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.RatingSource", b => + { + b.HasOne("Jellyfin.Data.Entities.MetadataProviderId", "Source") + .WithMany() + .HasForeignKey("MetadataProviderId_Source_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Release", b => + { + b.HasOne("Jellyfin.Data.Entities.Book", null) + .WithMany("Releases") + .HasForeignKey("Release_Releases_Id"); + + b.HasOne("Jellyfin.Data.Entities.CustomItem", null) + .WithMany("Releases") + .HasForeignKey("Release_Releases_Id") + .HasConstraintName("FK_Release_LibraryItem_Release_Releases_Id1"); + + b.HasOne("Jellyfin.Data.Entities.Episode", null) + .WithMany("Releases") + .HasForeignKey("Release_Releases_Id") + .HasConstraintName("FK_Release_LibraryItem_Release_Releases_Id2"); + + b.HasOne("Jellyfin.Data.Entities.Movie", null) + .WithMany("Releases") + .HasForeignKey("Release_Releases_Id") + .HasConstraintName("FK_Release_LibraryItem_Release_Releases_Id3"); + + b.HasOne("Jellyfin.Data.Entities.Photo", null) + .WithMany("Releases") + .HasForeignKey("Release_Releases_Id") + .HasConstraintName("FK_Release_LibraryItem_Release_Releases_Id4"); + + b.HasOne("Jellyfin.Data.Entities.Track", null) + .WithMany("Releases") + .HasForeignKey("Release_Releases_Id") + .HasConstraintName("FK_Release_LibraryItem_Release_Releases_Id5"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Episode", b => + { + b.HasOne("Jellyfin.Data.Entities.Season", null) + .WithMany("Episodes") + .HasForeignKey("Episode_Episodes_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Season", b => + { + b.HasOne("Jellyfin.Data.Entities.Series", null) + .WithMany("Seasons") + .HasForeignKey("Season_Seasons_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Track", b => + { + b.HasOne("Jellyfin.Data.Entities.MusicAlbum", null) + .WithMany("Tracks") + .HasForeignKey("Track_Tracks_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BookMetadata", b => + { + b.HasOne("Jellyfin.Data.Entities.Book", null) + .WithMany("BookMetadata") + .HasForeignKey("BookMetadata_BookMetadata_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CompanyMetadata", b => + { + b.HasOne("Jellyfin.Data.Entities.Company", null) + .WithMany("CompanyMetadata") + .HasForeignKey("CompanyMetadata_CompanyMetadata_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemMetadata", b => + { + b.HasOne("Jellyfin.Data.Entities.CustomItem", null) + .WithMany("CustomItemMetadata") + .HasForeignKey("CustomItemMetadata_CustomItemMetadata_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.EpisodeMetadata", b => + { + b.HasOne("Jellyfin.Data.Entities.Episode", null) + .WithMany("EpisodeMetadata") + .HasForeignKey("EpisodeMetadata_EpisodeMetadata_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MovieMetadata", b => + { + b.HasOne("Jellyfin.Data.Entities.Movie", null) + .WithMany("MovieMetadata") + .HasForeignKey("MovieMetadata_MovieMetadata_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MusicAlbumMetadata", b => + { + b.HasOne("Jellyfin.Data.Entities.MusicAlbum", null) + .WithMany("MusicAlbumMetadata") + .HasForeignKey("MusicAlbumMetadata_MusicAlbumMetadata_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PhotoMetadata", b => + { + b.HasOne("Jellyfin.Data.Entities.Photo", null) + .WithMany("PhotoMetadata") + .HasForeignKey("PhotoMetadata_PhotoMetadata_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.SeasonMetadata", b => + { + b.HasOne("Jellyfin.Data.Entities.Season", null) + .WithMany("SeasonMetadata") + .HasForeignKey("SeasonMetadata_SeasonMetadata_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.SeriesMetadata", b => + { + b.HasOne("Jellyfin.Data.Entities.Series", null) + .WithMany("SeriesMetadata") + .HasForeignKey("SeriesMetadata_SeriesMetadata_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.TrackMetadata", b => + { + b.HasOne("Jellyfin.Data.Entities.Track", null) + .WithMany("TrackMetadata") + .HasForeignKey("TrackMetadata_TrackMetadata_Id"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20200430215054_InitialSchema.cs b/Jellyfin.Server.Implementations/Migrations/20200430215054_InitialSchema.cs new file mode 100644 index 000000000..f6f2f1a81 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20200430215054_InitialSchema.cs @@ -0,0 +1,1294 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1601 + +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Jellyfin.Server.Implementations.Migrations +{ + public partial class InitialSchema : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "jellyfin"); + + migrationBuilder.CreateTable( + name: "ActivityLog", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(maxLength: 512, nullable: false), + Overview = table.Column(maxLength: 512, nullable: true), + ShortOverview = table.Column(maxLength: 512, nullable: true), + Type = table.Column(maxLength: 256, nullable: false), + UserId = table.Column(nullable: false), + ItemId = table.Column(maxLength: 256, nullable: true), + DateCreated = table.Column(nullable: false), + LogSeverity = table.Column(nullable: false), + RowVersion = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ActivityLog", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Collection", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(maxLength: 1024, nullable: true), + RowVersion = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Collection", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Library", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(maxLength: 1024, nullable: false), + RowVersion = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Library", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "MetadataProvider", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(maxLength: 1024, nullable: false), + RowVersion = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_MetadataProvider", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Person", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UrlId = table.Column(nullable: false), + Name = table.Column(maxLength: 1024, nullable: false), + SourceId = table.Column(maxLength: 255, nullable: true), + DateAdded = table.Column(nullable: false), + DateModified = table.Column(nullable: false), + RowVersion = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Person", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "User", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Username = table.Column(maxLength: 255, nullable: false), + Password = table.Column(maxLength: 65535, nullable: true), + MustUpdatePassword = table.Column(nullable: false), + AudioLanguagePreference = table.Column(maxLength: 255, nullable: false), + AuthenticationProviderId = table.Column(maxLength: 255, nullable: false), + GroupedFolders = table.Column(maxLength: 65535, nullable: true), + InvalidLoginAttemptCount = table.Column(nullable: false), + LatestItemExcludes = table.Column(maxLength: 65535, nullable: true), + LoginAttemptsBeforeLockout = table.Column(nullable: true), + MyMediaExcludes = table.Column(maxLength: 65535, nullable: true), + OrderedViews = table.Column(maxLength: 65535, nullable: true), + SubtitleMode = table.Column(maxLength: 255, nullable: false), + PlayDefaultAudioTrack = table.Column(nullable: false), + SubtitleLanguagePrefernce = table.Column(maxLength: 255, nullable: true), + DisplayMissingEpisodes = table.Column(nullable: true), + DisplayCollectionsView = table.Column(nullable: true), + HidePlayedInLatest = table.Column(nullable: true), + RememberAudioSelections = table.Column(nullable: true), + RememberSubtitleSelections = table.Column(nullable: true), + EnableNextEpisodeAutoPlay = table.Column(nullable: true), + EnableUserPreferenceAccess = table.Column(nullable: true), + RowVersion = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_User", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "LibraryRoot", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Path = table.Column(maxLength: 65535, nullable: false), + NetworkPath = table.Column(maxLength: 65535, nullable: true), + RowVersion = table.Column(nullable: false), + Library_Id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_LibraryRoot", x => x.Id); + table.ForeignKey( + name: "FK_LibraryRoot_Library_Library_Id", + column: x => x.Library_Id, + principalSchema: "jellyfin", + principalTable: "Library", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Group", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(maxLength: 255, nullable: false), + RowVersion = table.Column(nullable: false), + Group_Groups_Id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Group", x => x.Id); + table.ForeignKey( + name: "FK_Group_User_Group_Groups_Id", + column: x => x.Group_Groups_Id, + principalSchema: "jellyfin", + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "LibraryItem", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UrlId = table.Column(nullable: false), + DateAdded = table.Column(nullable: false), + RowVersion = table.Column(nullable: false), + LibraryRoot_Id = table.Column(nullable: true), + Discriminator = table.Column(nullable: false), + EpisodeNumber = table.Column(nullable: true), + Episode_Episodes_Id = table.Column(nullable: true), + SeasonNumber = table.Column(nullable: true), + Season_Seasons_Id = table.Column(nullable: true), + AirsDayOfWeek = table.Column(nullable: true), + AirsTime = table.Column(nullable: true), + FirstAired = table.Column(nullable: true), + TrackNumber = table.Column(nullable: true), + Track_Tracks_Id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_LibraryItem", x => x.Id); + table.ForeignKey( + name: "FK_LibraryItem_LibraryItem_Episode_Episodes_Id", + column: x => x.Episode_Episodes_Id, + principalSchema: "jellyfin", + principalTable: "LibraryItem", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_LibraryItem_LibraryRoot_LibraryRoot_Id", + column: x => x.LibraryRoot_Id, + principalSchema: "jellyfin", + principalTable: "LibraryRoot", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_LibraryItem_LibraryItem_Season_Seasons_Id", + column: x => x.Season_Seasons_Id, + principalSchema: "jellyfin", + principalTable: "LibraryItem", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_LibraryItem_LibraryItem_Track_Tracks_Id", + column: x => x.Track_Tracks_Id, + principalSchema: "jellyfin", + principalTable: "LibraryItem", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Permission", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Kind = table.Column(nullable: false), + Value = table.Column(nullable: false), + RowVersion = table.Column(nullable: false), + Permission_GroupPermissions_Id = table.Column(nullable: true), + Permission_Permissions_Id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Permission", x => x.Id); + table.ForeignKey( + name: "FK_Permission_Group_Permission_GroupPermissions_Id", + column: x => x.Permission_GroupPermissions_Id, + principalSchema: "jellyfin", + principalTable: "Group", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Permission_User_Permission_Permissions_Id", + column: x => x.Permission_Permissions_Id, + principalSchema: "jellyfin", + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Preference", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Kind = table.Column(nullable: false), + Value = table.Column(maxLength: 65535, nullable: false), + RowVersion = table.Column(nullable: false), + Preference_Preferences_Id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Preference", x => x.Id); + table.ForeignKey( + name: "FK_Preference_Group_Preference_Preferences_Id", + column: x => x.Preference_Preferences_Id, + principalSchema: "jellyfin", + principalTable: "Group", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Preference_User_Preference_Preferences_Id", + column: x => x.Preference_Preferences_Id, + principalSchema: "jellyfin", + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "ProviderMapping", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ProviderName = table.Column(maxLength: 255, nullable: false), + ProviderSecrets = table.Column(maxLength: 65535, nullable: false), + ProviderData = table.Column(maxLength: 65535, nullable: false), + RowVersion = table.Column(nullable: false), + ProviderMapping_ProviderMappings_Id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ProviderMapping", x => x.Id); + table.ForeignKey( + name: "FK_ProviderMapping_Group_ProviderMapping_ProviderMappings_Id", + column: x => x.ProviderMapping_ProviderMappings_Id, + principalSchema: "jellyfin", + principalTable: "Group", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_ProviderMapping_User_ProviderMapping_ProviderMappings_Id", + column: x => x.ProviderMapping_ProviderMappings_Id, + principalSchema: "jellyfin", + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "CollectionItem", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + RowVersion = table.Column(nullable: false), + LibraryItem_Id = table.Column(nullable: true), + CollectionItem_Next_Id = table.Column(nullable: true), + CollectionItem_Previous_Id = table.Column(nullable: true), + CollectionItem_CollectionItem_Id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_CollectionItem", x => x.Id); + table.ForeignKey( + name: "FK_CollectionItem_Collection_CollectionItem_CollectionItem_Id", + column: x => x.CollectionItem_CollectionItem_Id, + principalSchema: "jellyfin", + principalTable: "Collection", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_CollectionItem_CollectionItem_CollectionItem_Next_Id", + column: x => x.CollectionItem_Next_Id, + principalSchema: "jellyfin", + principalTable: "CollectionItem", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_CollectionItem_CollectionItem_CollectionItem_Previous_Id", + column: x => x.CollectionItem_Previous_Id, + principalSchema: "jellyfin", + principalTable: "CollectionItem", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_CollectionItem_LibraryItem_LibraryItem_Id", + column: x => x.LibraryItem_Id, + principalSchema: "jellyfin", + principalTable: "LibraryItem", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Release", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(maxLength: 1024, nullable: false), + RowVersion = table.Column(nullable: false), + Release_Releases_Id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Release", x => x.Id); + table.ForeignKey( + name: "FK_Release_LibraryItem_Release_Releases_Id", + column: x => x.Release_Releases_Id, + principalSchema: "jellyfin", + principalTable: "LibraryItem", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Release_LibraryItem_Release_Releases_Id1", + column: x => x.Release_Releases_Id, + principalSchema: "jellyfin", + principalTable: "LibraryItem", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Release_LibraryItem_Release_Releases_Id2", + column: x => x.Release_Releases_Id, + principalSchema: "jellyfin", + principalTable: "LibraryItem", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Release_LibraryItem_Release_Releases_Id3", + column: x => x.Release_Releases_Id, + principalSchema: "jellyfin", + principalTable: "LibraryItem", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Release_LibraryItem_Release_Releases_Id4", + column: x => x.Release_Releases_Id, + principalSchema: "jellyfin", + principalTable: "LibraryItem", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Release_LibraryItem_Release_Releases_Id5", + column: x => x.Release_Releases_Id, + principalSchema: "jellyfin", + principalTable: "LibraryItem", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Chapter", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(maxLength: 1024, nullable: true), + Language = table.Column(maxLength: 3, nullable: false), + TimeStart = table.Column(nullable: false), + TimeEnd = table.Column(nullable: true), + RowVersion = table.Column(nullable: false), + Chapter_Chapters_Id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Chapter", x => x.Id); + table.ForeignKey( + name: "FK_Chapter_Release_Chapter_Chapters_Id", + column: x => x.Chapter_Chapters_Id, + principalSchema: "jellyfin", + principalTable: "Release", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "MediaFile", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Path = table.Column(maxLength: 65535, nullable: false), + Kind = table.Column(nullable: false), + RowVersion = table.Column(nullable: false), + MediaFile_MediaFiles_Id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MediaFile", x => x.Id); + table.ForeignKey( + name: "FK_MediaFile_Release_MediaFile_MediaFiles_Id", + column: x => x.MediaFile_MediaFiles_Id, + principalSchema: "jellyfin", + principalTable: "Release", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "MediaFileStream", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + StreamNumber = table.Column(nullable: false), + RowVersion = table.Column(nullable: false), + MediaFileStream_MediaFileStreams_Id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MediaFileStream", x => x.Id); + table.ForeignKey( + name: "FK_MediaFileStream_MediaFile_MediaFileStream_MediaFileStreams_Id", + column: x => x.MediaFileStream_MediaFileStreams_Id, + principalSchema: "jellyfin", + principalTable: "MediaFile", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "PersonRole", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Role = table.Column(maxLength: 1024, nullable: true), + Type = table.Column(nullable: false), + RowVersion = table.Column(nullable: false), + Person_Id = table.Column(nullable: true), + Artwork_Artwork_Id = table.Column(nullable: true), + PersonRole_PersonRoles_Id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PersonRole", x => x.Id); + table.ForeignKey( + name: "FK_PersonRole_Person_Person_Id", + column: x => x.Person_Id, + principalSchema: "jellyfin", + principalTable: "Person", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Metadata", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Title = table.Column(maxLength: 1024, nullable: false), + OriginalTitle = table.Column(maxLength: 1024, nullable: true), + SortTitle = table.Column(maxLength: 1024, nullable: true), + Language = table.Column(maxLength: 3, nullable: false), + ReleaseDate = table.Column(nullable: true), + DateAdded = table.Column(nullable: false), + DateModified = table.Column(nullable: false), + RowVersion = table.Column(nullable: false), + Discriminator = table.Column(nullable: false), + ISBN = table.Column(nullable: true), + BookMetadata_BookMetadata_Id = table.Column(nullable: true), + Description = table.Column(maxLength: 65535, nullable: true), + Headquarters = table.Column(maxLength: 255, nullable: true), + Country = table.Column(maxLength: 2, nullable: true), + Homepage = table.Column(maxLength: 1024, nullable: true), + CompanyMetadata_CompanyMetadata_Id = table.Column(nullable: true), + CustomItemMetadata_CustomItemMetadata_Id = table.Column(nullable: true), + Outline = table.Column(maxLength: 1024, nullable: true), + Plot = table.Column(maxLength: 65535, nullable: true), + Tagline = table.Column(maxLength: 1024, nullable: true), + EpisodeMetadata_EpisodeMetadata_Id = table.Column(nullable: true), + MovieMetadata_Outline = table.Column(maxLength: 1024, nullable: true), + MovieMetadata_Plot = table.Column(maxLength: 65535, nullable: true), + MovieMetadata_Tagline = table.Column(maxLength: 1024, nullable: true), + MovieMetadata_Country = table.Column(maxLength: 2, nullable: true), + MovieMetadata_MovieMetadata_Id = table.Column(nullable: true), + Barcode = table.Column(maxLength: 255, nullable: true), + LabelNumber = table.Column(maxLength: 255, nullable: true), + MusicAlbumMetadata_Country = table.Column(maxLength: 2, nullable: true), + MusicAlbumMetadata_MusicAlbumMetadata_Id = table.Column(nullable: true), + PhotoMetadata_PhotoMetadata_Id = table.Column(nullable: true), + SeasonMetadata_Outline = table.Column(maxLength: 1024, nullable: true), + SeasonMetadata_SeasonMetadata_Id = table.Column(nullable: true), + SeriesMetadata_Outline = table.Column(maxLength: 1024, nullable: true), + SeriesMetadata_Plot = table.Column(maxLength: 65535, nullable: true), + SeriesMetadata_Tagline = table.Column(maxLength: 1024, nullable: true), + SeriesMetadata_Country = table.Column(maxLength: 2, nullable: true), + SeriesMetadata_SeriesMetadata_Id = table.Column(nullable: true), + TrackMetadata_TrackMetadata_Id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Metadata", x => x.Id); + table.ForeignKey( + name: "FK_Metadata_LibraryItem_BookMetadata_BookMetadata_Id", + column: x => x.BookMetadata_BookMetadata_Id, + principalSchema: "jellyfin", + principalTable: "LibraryItem", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Metadata_LibraryItem_CustomItemMetadata_CustomItemMetadata_Id", + column: x => x.CustomItemMetadata_CustomItemMetadata_Id, + principalSchema: "jellyfin", + principalTable: "LibraryItem", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Metadata_LibraryItem_EpisodeMetadata_EpisodeMetadata_Id", + column: x => x.EpisodeMetadata_EpisodeMetadata_Id, + principalSchema: "jellyfin", + principalTable: "LibraryItem", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Metadata_LibraryItem_MovieMetadata_MovieMetadata_Id", + column: x => x.MovieMetadata_MovieMetadata_Id, + principalSchema: "jellyfin", + principalTable: "LibraryItem", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Metadata_LibraryItem_MusicAlbumMetadata_MusicAlbumMetadata_Id", + column: x => x.MusicAlbumMetadata_MusicAlbumMetadata_Id, + principalSchema: "jellyfin", + principalTable: "LibraryItem", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Metadata_LibraryItem_PhotoMetadata_PhotoMetadata_Id", + column: x => x.PhotoMetadata_PhotoMetadata_Id, + principalSchema: "jellyfin", + principalTable: "LibraryItem", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Metadata_LibraryItem_SeasonMetadata_SeasonMetadata_Id", + column: x => x.SeasonMetadata_SeasonMetadata_Id, + principalSchema: "jellyfin", + principalTable: "LibraryItem", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Metadata_LibraryItem_SeriesMetadata_SeriesMetadata_Id", + column: x => x.SeriesMetadata_SeriesMetadata_Id, + principalSchema: "jellyfin", + principalTable: "LibraryItem", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Metadata_LibraryItem_TrackMetadata_TrackMetadata_Id", + column: x => x.TrackMetadata_TrackMetadata_Id, + principalSchema: "jellyfin", + principalTable: "LibraryItem", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Artwork", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Path = table.Column(maxLength: 65535, nullable: false), + Kind = table.Column(nullable: false), + RowVersion = table.Column(nullable: false), + PersonRole_PersonRoles_Id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Artwork", x => x.Id); + table.ForeignKey( + name: "FK_Artwork_Metadata_PersonRole_PersonRoles_Id", + column: x => x.PersonRole_PersonRoles_Id, + principalSchema: "jellyfin", + principalTable: "Metadata", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Company", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + RowVersion = table.Column(nullable: false), + Company_Parent_Id = table.Column(nullable: true), + Company_Labels_Id = table.Column(nullable: true), + Company_Networks_Id = table.Column(nullable: true), + Company_Publishers_Id = table.Column(nullable: true), + Company_Studios_Id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Company", x => x.Id); + table.ForeignKey( + name: "FK_Company_Metadata_Company_Labels_Id", + column: x => x.Company_Labels_Id, + principalSchema: "jellyfin", + principalTable: "Metadata", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Company_Metadata_Company_Networks_Id", + column: x => x.Company_Networks_Id, + principalSchema: "jellyfin", + principalTable: "Metadata", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Company_Company_Company_Parent_Id", + column: x => x.Company_Parent_Id, + principalSchema: "jellyfin", + principalTable: "Company", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Company_Metadata_Company_Publishers_Id", + column: x => x.Company_Publishers_Id, + principalSchema: "jellyfin", + principalTable: "Metadata", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Company_Metadata_Company_Studios_Id", + column: x => x.Company_Studios_Id, + principalSchema: "jellyfin", + principalTable: "Metadata", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Genre", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(maxLength: 255, nullable: false), + RowVersion = table.Column(nullable: false), + PersonRole_PersonRoles_Id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Genre", x => x.Id); + table.ForeignKey( + name: "FK_Genre_Metadata_PersonRole_PersonRoles_Id", + column: x => x.PersonRole_PersonRoles_Id, + principalSchema: "jellyfin", + principalTable: "Metadata", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "MetadataProviderId", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ProviderId = table.Column(maxLength: 255, nullable: false), + RowVersion = table.Column(nullable: false), + MetadataProvider_Id = table.Column(nullable: true), + MetadataProviderId_Sources_Id = table.Column(nullable: true), + PersonRole_PersonRoles_Id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MetadataProviderId", x => x.Id); + table.ForeignKey( + name: "FK_MetadataProviderId_Person_MetadataProviderId_Sources_Id", + column: x => x.MetadataProviderId_Sources_Id, + principalSchema: "jellyfin", + principalTable: "Person", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_MetadataProviderId_PersonRole_MetadataProviderId_Sources_Id", + column: x => x.MetadataProviderId_Sources_Id, + principalSchema: "jellyfin", + principalTable: "PersonRole", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_MetadataProviderId_MetadataProvider_MetadataProvider_Id", + column: x => x.MetadataProvider_Id, + principalSchema: "jellyfin", + principalTable: "MetadataProvider", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_MetadataProviderId_Metadata_PersonRole_PersonRoles_Id", + column: x => x.PersonRole_PersonRoles_Id, + principalSchema: "jellyfin", + principalTable: "Metadata", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "RatingSource", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(maxLength: 1024, nullable: true), + MaximumValue = table.Column(nullable: false), + MinimumValue = table.Column(nullable: false), + RowVersion = table.Column(nullable: false), + MetadataProviderId_Source_Id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_RatingSource", x => x.Id); + table.ForeignKey( + name: "FK_RatingSource_MetadataProviderId_MetadataProviderId_Source_Id", + column: x => x.MetadataProviderId_Source_Id, + principalSchema: "jellyfin", + principalTable: "MetadataProviderId", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Rating", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Value = table.Column(nullable: false), + Votes = table.Column(nullable: true), + RowVersion = table.Column(nullable: false), + RatingSource_RatingType_Id = table.Column(nullable: true), + PersonRole_PersonRoles_Id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Rating", x => x.Id); + table.ForeignKey( + name: "FK_Rating_Metadata_PersonRole_PersonRoles_Id", + column: x => x.PersonRole_PersonRoles_Id, + principalSchema: "jellyfin", + principalTable: "Metadata", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Rating_RatingSource_RatingSource_RatingType_Id", + column: x => x.RatingSource_RatingType_Id, + principalSchema: "jellyfin", + principalTable: "RatingSource", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_Artwork_Kind", + schema: "jellyfin", + table: "Artwork", + column: "Kind"); + + migrationBuilder.CreateIndex( + name: "IX_Artwork_PersonRole_PersonRoles_Id", + schema: "jellyfin", + table: "Artwork", + column: "PersonRole_PersonRoles_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Chapter_Chapter_Chapters_Id", + schema: "jellyfin", + table: "Chapter", + column: "Chapter_Chapters_Id"); + + migrationBuilder.CreateIndex( + name: "IX_CollectionItem_CollectionItem_CollectionItem_Id", + schema: "jellyfin", + table: "CollectionItem", + column: "CollectionItem_CollectionItem_Id"); + + migrationBuilder.CreateIndex( + name: "IX_CollectionItem_CollectionItem_Next_Id", + schema: "jellyfin", + table: "CollectionItem", + column: "CollectionItem_Next_Id"); + + migrationBuilder.CreateIndex( + name: "IX_CollectionItem_CollectionItem_Previous_Id", + schema: "jellyfin", + table: "CollectionItem", + column: "CollectionItem_Previous_Id"); + + migrationBuilder.CreateIndex( + name: "IX_CollectionItem_LibraryItem_Id", + schema: "jellyfin", + table: "CollectionItem", + column: "LibraryItem_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Company_Company_Labels_Id", + schema: "jellyfin", + table: "Company", + column: "Company_Labels_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Company_Company_Networks_Id", + schema: "jellyfin", + table: "Company", + column: "Company_Networks_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Company_Company_Parent_Id", + schema: "jellyfin", + table: "Company", + column: "Company_Parent_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Company_Company_Publishers_Id", + schema: "jellyfin", + table: "Company", + column: "Company_Publishers_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Company_Company_Studios_Id", + schema: "jellyfin", + table: "Company", + column: "Company_Studios_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Genre_Name", + schema: "jellyfin", + table: "Genre", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Genre_PersonRole_PersonRoles_Id", + schema: "jellyfin", + table: "Genre", + column: "PersonRole_PersonRoles_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Group_Group_Groups_Id", + schema: "jellyfin", + table: "Group", + column: "Group_Groups_Id"); + + migrationBuilder.CreateIndex( + name: "IX_LibraryItem_Episode_Episodes_Id", + schema: "jellyfin", + table: "LibraryItem", + column: "Episode_Episodes_Id"); + + migrationBuilder.CreateIndex( + name: "IX_LibraryItem_LibraryRoot_Id", + schema: "jellyfin", + table: "LibraryItem", + column: "LibraryRoot_Id"); + + migrationBuilder.CreateIndex( + name: "IX_LibraryItem_UrlId", + schema: "jellyfin", + table: "LibraryItem", + column: "UrlId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_LibraryItem_Season_Seasons_Id", + schema: "jellyfin", + table: "LibraryItem", + column: "Season_Seasons_Id"); + + migrationBuilder.CreateIndex( + name: "IX_LibraryItem_Track_Tracks_Id", + schema: "jellyfin", + table: "LibraryItem", + column: "Track_Tracks_Id"); + + migrationBuilder.CreateIndex( + name: "IX_LibraryRoot_Library_Id", + schema: "jellyfin", + table: "LibraryRoot", + column: "Library_Id"); + + migrationBuilder.CreateIndex( + name: "IX_MediaFile_MediaFile_MediaFiles_Id", + schema: "jellyfin", + table: "MediaFile", + column: "MediaFile_MediaFiles_Id"); + + migrationBuilder.CreateIndex( + name: "IX_MediaFileStream_MediaFileStream_MediaFileStreams_Id", + schema: "jellyfin", + table: "MediaFileStream", + column: "MediaFileStream_MediaFileStreams_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Metadata_BookMetadata_BookMetadata_Id", + schema: "jellyfin", + table: "Metadata", + column: "BookMetadata_BookMetadata_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Metadata_CompanyMetadata_CompanyMetadata_Id", + schema: "jellyfin", + table: "Metadata", + column: "CompanyMetadata_CompanyMetadata_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Metadata_CustomItemMetadata_CustomItemMetadata_Id", + schema: "jellyfin", + table: "Metadata", + column: "CustomItemMetadata_CustomItemMetadata_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Metadata_EpisodeMetadata_EpisodeMetadata_Id", + schema: "jellyfin", + table: "Metadata", + column: "EpisodeMetadata_EpisodeMetadata_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Metadata_MovieMetadata_MovieMetadata_Id", + schema: "jellyfin", + table: "Metadata", + column: "MovieMetadata_MovieMetadata_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Metadata_MusicAlbumMetadata_MusicAlbumMetadata_Id", + schema: "jellyfin", + table: "Metadata", + column: "MusicAlbumMetadata_MusicAlbumMetadata_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Metadata_PhotoMetadata_PhotoMetadata_Id", + schema: "jellyfin", + table: "Metadata", + column: "PhotoMetadata_PhotoMetadata_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Metadata_SeasonMetadata_SeasonMetadata_Id", + schema: "jellyfin", + table: "Metadata", + column: "SeasonMetadata_SeasonMetadata_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Metadata_SeriesMetadata_SeriesMetadata_Id", + schema: "jellyfin", + table: "Metadata", + column: "SeriesMetadata_SeriesMetadata_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Metadata_TrackMetadata_TrackMetadata_Id", + schema: "jellyfin", + table: "Metadata", + column: "TrackMetadata_TrackMetadata_Id"); + + migrationBuilder.CreateIndex( + name: "IX_MetadataProviderId_MetadataProviderId_Sources_Id", + schema: "jellyfin", + table: "MetadataProviderId", + column: "MetadataProviderId_Sources_Id"); + + migrationBuilder.CreateIndex( + name: "IX_MetadataProviderId_MetadataProvider_Id", + schema: "jellyfin", + table: "MetadataProviderId", + column: "MetadataProvider_Id"); + + migrationBuilder.CreateIndex( + name: "IX_MetadataProviderId_PersonRole_PersonRoles_Id", + schema: "jellyfin", + table: "MetadataProviderId", + column: "PersonRole_PersonRoles_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Permission_Permission_GroupPermissions_Id", + schema: "jellyfin", + table: "Permission", + column: "Permission_GroupPermissions_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Permission_Permission_Permissions_Id", + schema: "jellyfin", + table: "Permission", + column: "Permission_Permissions_Id"); + + migrationBuilder.CreateIndex( + name: "IX_PersonRole_Artwork_Artwork_Id", + schema: "jellyfin", + table: "PersonRole", + column: "Artwork_Artwork_Id"); + + migrationBuilder.CreateIndex( + name: "IX_PersonRole_PersonRole_PersonRoles_Id", + schema: "jellyfin", + table: "PersonRole", + column: "PersonRole_PersonRoles_Id"); + + migrationBuilder.CreateIndex( + name: "IX_PersonRole_Person_Id", + schema: "jellyfin", + table: "PersonRole", + column: "Person_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Preference_Preference_Preferences_Id", + schema: "jellyfin", + table: "Preference", + column: "Preference_Preferences_Id"); + + migrationBuilder.CreateIndex( + name: "IX_ProviderMapping_ProviderMapping_ProviderMappings_Id", + schema: "jellyfin", + table: "ProviderMapping", + column: "ProviderMapping_ProviderMappings_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Rating_PersonRole_PersonRoles_Id", + schema: "jellyfin", + table: "Rating", + column: "PersonRole_PersonRoles_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Rating_RatingSource_RatingType_Id", + schema: "jellyfin", + table: "Rating", + column: "RatingSource_RatingType_Id"); + + migrationBuilder.CreateIndex( + name: "IX_RatingSource_MetadataProviderId_Source_Id", + schema: "jellyfin", + table: "RatingSource", + column: "MetadataProviderId_Source_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Release_Release_Releases_Id", + schema: "jellyfin", + table: "Release", + column: "Release_Releases_Id"); + + migrationBuilder.AddForeignKey( + name: "FK_PersonRole_Metadata_PersonRole_PersonRoles_Id", + schema: "jellyfin", + table: "PersonRole", + column: "PersonRole_PersonRoles_Id", + principalSchema: "jellyfin", + principalTable: "Metadata", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_PersonRole_Artwork_Artwork_Artwork_Id", + schema: "jellyfin", + table: "PersonRole", + column: "Artwork_Artwork_Id", + principalSchema: "jellyfin", + principalTable: "Artwork", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_Metadata_Company_CompanyMetadata_CompanyMetadata_Id", + schema: "jellyfin", + table: "Metadata", + column: "CompanyMetadata_CompanyMetadata_Id", + principalSchema: "jellyfin", + principalTable: "Company", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Company_Metadata_Company_Labels_Id", + schema: "jellyfin", + table: "Company"); + + migrationBuilder.DropForeignKey( + name: "FK_Company_Metadata_Company_Networks_Id", + schema: "jellyfin", + table: "Company"); + + migrationBuilder.DropForeignKey( + name: "FK_Company_Metadata_Company_Publishers_Id", + schema: "jellyfin", + table: "Company"); + + migrationBuilder.DropForeignKey( + name: "FK_Company_Metadata_Company_Studios_Id", + schema: "jellyfin", + table: "Company"); + + migrationBuilder.DropTable( + name: "ActivityLog", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "Chapter", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "CollectionItem", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "Genre", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "MediaFileStream", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "Permission", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "Preference", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "ProviderMapping", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "Rating", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "Collection", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "MediaFile", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "Group", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "RatingSource", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "Release", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "User", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "MetadataProviderId", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "PersonRole", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "MetadataProvider", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "Artwork", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "Person", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "Metadata", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "LibraryItem", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "Company", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "LibraryRoot", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "Library", + schema: "jellyfin"); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs new file mode 100644 index 000000000..72a4a8c3b --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// + /// The design time factory for . + /// This is only used for the creation of migrations and not during runtime. + /// + internal class DesignTimeJellyfinDbFactory : IDesignTimeDbContextFactory + { + public JellyfinDb CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSqlite("Data Source=jellyfin.db"); + + return new JellyfinDb(optionsBuilder.Options); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs new file mode 100644 index 000000000..8cdd101af --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -0,0 +1,1511 @@ +#pragma warning disable CS1591 + +// +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDb))] + partial class JellyfinDbModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("jellyfin") + .HasAnnotation("ProductVersion", "3.1.3"); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("Overview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ActivityLog"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Artwork", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("PersonRole_PersonRoles_Id") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Kind"); + + b.HasIndex("PersonRole_PersonRoles_Id"); + + b.ToTable("Artwork"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Chapter_Chapters_Id") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(3); + + b.Property("Name") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("TimeEnd") + .HasColumnType("INTEGER"); + + b.Property("TimeStart") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Chapter_Chapters_Id"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Collection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Collection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CollectionItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CollectionItem_CollectionItem_Id") + .HasColumnType("INTEGER"); + + b.Property("CollectionItem_Next_Id") + .HasColumnType("INTEGER"); + + b.Property("CollectionItem_Previous_Id") + .HasColumnType("INTEGER"); + + b.Property("LibraryItem_Id") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionItem_CollectionItem_Id"); + + b.HasIndex("CollectionItem_Next_Id"); + + b.HasIndex("CollectionItem_Previous_Id"); + + b.HasIndex("LibraryItem_Id"); + + b.ToTable("CollectionItem"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Company_Labels_Id") + .HasColumnType("INTEGER"); + + b.Property("Company_Networks_Id") + .HasColumnType("INTEGER"); + + b.Property("Company_Parent_Id") + .HasColumnType("INTEGER"); + + b.Property("Company_Publishers_Id") + .HasColumnType("INTEGER"); + + b.Property("Company_Studios_Id") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Company_Labels_Id"); + + b.HasIndex("Company_Networks_Id"); + + b.HasIndex("Company_Parent_Id"); + + b.HasIndex("Company_Publishers_Id"); + + b.HasIndex("Company_Studios_Id"); + + b.ToTable("Company"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("PersonRole_PersonRoles_Id") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("PersonRole_PersonRoles_Id"); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Group_Groups_Id") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Group_Groups_Id"); + + b.ToTable("Group"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.LibraryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LibraryRoot_Id") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UrlId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryRoot_Id"); + + b.HasIndex("UrlId") + .IsUnique(); + + b.ToTable("LibraryItem"); + + b.HasDiscriminator("Discriminator").HasValue("LibraryItem"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.LibraryRoot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Library_Id") + .HasColumnType("INTEGER"); + + b.Property("NetworkPath") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Library_Id"); + + b.ToTable("LibraryRoot"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("MediaFile_MediaFiles_Id") + .HasColumnType("INTEGER"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MediaFile_MediaFiles_Id"); + + b.ToTable("MediaFile"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaFileStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MediaFileStream_MediaFileStreams_Id") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("StreamNumber") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MediaFileStream_MediaFileStreams_Id"); + + b.ToTable("MediaFileStream"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Metadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(3); + + b.Property("OriginalTitle") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SortTitle") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.HasKey("Id"); + + b.ToTable("Metadata"); + + b.HasDiscriminator("Discriminator").HasValue("Metadata"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MetadataProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MetadataProvider"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MetadataProviderId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MetadataProviderId_Sources_Id") + .HasColumnType("INTEGER"); + + b.Property("MetadataProvider_Id") + .HasColumnType("INTEGER"); + + b.Property("PersonRole_PersonRoles_Id") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MetadataProviderId_Sources_Id"); + + b.HasIndex("MetadataProvider_Id"); + + b.HasIndex("PersonRole_PersonRoles_Id"); + + b.ToTable("MetadataProviderId"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_GroupPermissions_Id") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Id") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Permission_GroupPermissions_Id"); + + b.HasIndex("Permission_Permissions_Id"); + + b.ToTable("Permission"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SourceId") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UrlId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PersonRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Artwork_Artwork_Id") + .HasColumnType("INTEGER"); + + b.Property("PersonRole_PersonRoles_Id") + .HasColumnType("INTEGER"); + + b.Property("Person_Id") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Artwork_Artwork_Id"); + + b.HasIndex("PersonRole_PersonRoles_Id"); + + b.HasIndex("Person_Id"); + + b.ToTable("PersonRole"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Id") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.HasKey("Id"); + + b.HasIndex("Preference_Preferences_Id"); + + b.ToTable("Preference"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ProviderMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ProviderData") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("ProviderMapping_ProviderMappings_Id") + .HasColumnType("INTEGER"); + + b.Property("ProviderName") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("ProviderSecrets") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderMapping_ProviderMappings_Id"); + + b.ToTable("ProviderMapping"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Rating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("PersonRole_PersonRoles_Id") + .HasColumnType("INTEGER"); + + b.Property("RatingSource_RatingType_Id") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("REAL"); + + b.Property("Votes") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PersonRole_PersonRoles_Id"); + + b.HasIndex("RatingSource_RatingType_Id"); + + b.ToTable("Rating"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.RatingSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MaximumValue") + .HasColumnType("REAL"); + + b.Property("MetadataProviderId_Source_Id") + .HasColumnType("INTEGER"); + + b.Property("MinimumValue") + .HasColumnType("REAL"); + + b.Property("Name") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MetadataProviderId_Source_Id"); + + b.ToTable("RatingSource"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Release", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("Release_Releases_Id") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Release_Releases_Id"); + + b.ToTable("Release"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudioLanguagePreference") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("GroupedFolders") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LatestItemExcludes") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("MyMediaExcludes") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("OrderedViews") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("Password") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePrefernce") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("SubtitleMode") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Book", b => + { + b.HasBaseType("Jellyfin.Data.Entities.LibraryItem"); + + b.ToTable("Book"); + + b.HasDiscriminator().HasValue("Book"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItem", b => + { + b.HasBaseType("Jellyfin.Data.Entities.LibraryItem"); + + b.ToTable("LibraryItem"); + + b.HasDiscriminator().HasValue("CustomItem"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Episode", b => + { + b.HasBaseType("Jellyfin.Data.Entities.LibraryItem"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("Episode_Episodes_Id") + .HasColumnType("INTEGER"); + + b.HasIndex("Episode_Episodes_Id"); + + b.ToTable("Episode"); + + b.HasDiscriminator().HasValue("Episode"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Movie", b => + { + b.HasBaseType("Jellyfin.Data.Entities.LibraryItem"); + + b.ToTable("Movie"); + + b.HasDiscriminator().HasValue("Movie"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MusicAlbum", b => + { + b.HasBaseType("Jellyfin.Data.Entities.LibraryItem"); + + b.ToTable("MusicAlbum"); + + b.HasDiscriminator().HasValue("MusicAlbum"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Photo", b => + { + b.HasBaseType("Jellyfin.Data.Entities.LibraryItem"); + + b.ToTable("Photo"); + + b.HasDiscriminator().HasValue("Photo"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Season", b => + { + b.HasBaseType("Jellyfin.Data.Entities.LibraryItem"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("Season_Seasons_Id") + .HasColumnType("INTEGER"); + + b.HasIndex("Season_Seasons_Id"); + + b.ToTable("Season"); + + b.HasDiscriminator().HasValue("Season"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Series", b => + { + b.HasBaseType("Jellyfin.Data.Entities.LibraryItem"); + + b.Property("AirsDayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("AirsTime") + .HasColumnType("TEXT"); + + b.Property("FirstAired") + .HasColumnType("TEXT"); + + b.ToTable("Series"); + + b.HasDiscriminator().HasValue("Series"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Track", b => + { + b.HasBaseType("Jellyfin.Data.Entities.LibraryItem"); + + b.Property("TrackNumber") + .HasColumnType("INTEGER"); + + b.Property("Track_Tracks_Id") + .HasColumnType("INTEGER"); + + b.HasIndex("Track_Tracks_Id"); + + b.ToTable("Track"); + + b.HasDiscriminator().HasValue("Track"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BookMetadata", b => + { + b.HasBaseType("Jellyfin.Data.Entities.Metadata"); + + b.Property("BookMetadata_BookMetadata_Id") + .HasColumnType("INTEGER"); + + b.Property("ISBN") + .HasColumnType("INTEGER"); + + b.HasIndex("BookMetadata_BookMetadata_Id"); + + b.ToTable("Metadata"); + + b.HasDiscriminator().HasValue("BookMetadata"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CompanyMetadata", b => + { + b.HasBaseType("Jellyfin.Data.Entities.Metadata"); + + b.Property("CompanyMetadata_CompanyMetadata_Id") + .HasColumnType("INTEGER"); + + b.Property("Country") + .HasColumnType("TEXT") + .HasMaxLength(2); + + b.Property("Description") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("Headquarters") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Homepage") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.HasIndex("CompanyMetadata_CompanyMetadata_Id"); + + b.ToTable("CompanyMetadata"); + + b.HasDiscriminator().HasValue("CompanyMetadata"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemMetadata", b => + { + b.HasBaseType("Jellyfin.Data.Entities.Metadata"); + + b.Property("CustomItemMetadata_CustomItemMetadata_Id") + .HasColumnType("INTEGER"); + + b.HasIndex("CustomItemMetadata_CustomItemMetadata_Id"); + + b.ToTable("CustomItemMetadata"); + + b.HasDiscriminator().HasValue("CustomItemMetadata"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.EpisodeMetadata", b => + { + b.HasBaseType("Jellyfin.Data.Entities.Metadata"); + + b.Property("EpisodeMetadata_EpisodeMetadata_Id") + .HasColumnType("INTEGER"); + + b.Property("Outline") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("Plot") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("Tagline") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.HasIndex("EpisodeMetadata_EpisodeMetadata_Id"); + + b.ToTable("EpisodeMetadata"); + + b.HasDiscriminator().HasValue("EpisodeMetadata"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MovieMetadata", b => + { + b.HasBaseType("Jellyfin.Data.Entities.Metadata"); + + b.Property("Country") + .HasColumnName("MovieMetadata_Country") + .HasColumnType("TEXT") + .HasMaxLength(2); + + b.Property("MovieMetadata_MovieMetadata_Id") + .HasColumnType("INTEGER"); + + b.Property("Outline") + .HasColumnName("MovieMetadata_Outline") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("Plot") + .HasColumnName("MovieMetadata_Plot") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("Tagline") + .HasColumnName("MovieMetadata_Tagline") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.HasIndex("MovieMetadata_MovieMetadata_Id"); + + b.ToTable("MovieMetadata"); + + b.HasDiscriminator().HasValue("MovieMetadata"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MusicAlbumMetadata", b => + { + b.HasBaseType("Jellyfin.Data.Entities.Metadata"); + + b.Property("Barcode") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Country") + .HasColumnName("MusicAlbumMetadata_Country") + .HasColumnType("TEXT") + .HasMaxLength(2); + + b.Property("LabelNumber") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("MusicAlbumMetadata_MusicAlbumMetadata_Id") + .HasColumnType("INTEGER"); + + b.HasIndex("MusicAlbumMetadata_MusicAlbumMetadata_Id"); + + b.ToTable("MusicAlbumMetadata"); + + b.HasDiscriminator().HasValue("MusicAlbumMetadata"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PhotoMetadata", b => + { + b.HasBaseType("Jellyfin.Data.Entities.Metadata"); + + b.Property("PhotoMetadata_PhotoMetadata_Id") + .HasColumnType("INTEGER"); + + b.HasIndex("PhotoMetadata_PhotoMetadata_Id"); + + b.ToTable("PhotoMetadata"); + + b.HasDiscriminator().HasValue("PhotoMetadata"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.SeasonMetadata", b => + { + b.HasBaseType("Jellyfin.Data.Entities.Metadata"); + + b.Property("Outline") + .HasColumnName("SeasonMetadata_Outline") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("SeasonMetadata_SeasonMetadata_Id") + .HasColumnType("INTEGER"); + + b.HasIndex("SeasonMetadata_SeasonMetadata_Id"); + + b.ToTable("SeasonMetadata"); + + b.HasDiscriminator().HasValue("SeasonMetadata"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.SeriesMetadata", b => + { + b.HasBaseType("Jellyfin.Data.Entities.Metadata"); + + b.Property("Country") + .HasColumnName("SeriesMetadata_Country") + .HasColumnType("TEXT") + .HasMaxLength(2); + + b.Property("Outline") + .HasColumnName("SeriesMetadata_Outline") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.Property("Plot") + .HasColumnName("SeriesMetadata_Plot") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("SeriesMetadata_SeriesMetadata_Id") + .HasColumnType("INTEGER"); + + b.Property("Tagline") + .HasColumnName("SeriesMetadata_Tagline") + .HasColumnType("TEXT") + .HasMaxLength(1024); + + b.HasIndex("SeriesMetadata_SeriesMetadata_Id"); + + b.ToTable("SeriesMetadata"); + + b.HasDiscriminator().HasValue("SeriesMetadata"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.TrackMetadata", b => + { + b.HasBaseType("Jellyfin.Data.Entities.Metadata"); + + b.Property("TrackMetadata_TrackMetadata_Id") + .HasColumnType("INTEGER"); + + b.HasIndex("TrackMetadata_TrackMetadata_Id"); + + b.ToTable("TrackMetadata"); + + b.HasDiscriminator().HasValue("TrackMetadata"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Artwork", b => + { + b.HasOne("Jellyfin.Data.Entities.Metadata", null) + .WithMany("Artwork") + .HasForeignKey("PersonRole_PersonRoles_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.HasOne("Jellyfin.Data.Entities.Release", null) + .WithMany("Chapters") + .HasForeignKey("Chapter_Chapters_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CollectionItem", b => + { + b.HasOne("Jellyfin.Data.Entities.Collection", null) + .WithMany("CollectionItem") + .HasForeignKey("CollectionItem_CollectionItem_Id"); + + b.HasOne("Jellyfin.Data.Entities.CollectionItem", "Next") + .WithMany() + .HasForeignKey("CollectionItem_Next_Id"); + + b.HasOne("Jellyfin.Data.Entities.CollectionItem", "Previous") + .WithMany() + .HasForeignKey("CollectionItem_Previous_Id"); + + b.HasOne("Jellyfin.Data.Entities.LibraryItem", "LibraryItem") + .WithMany() + .HasForeignKey("LibraryItem_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Company", b => + { + b.HasOne("Jellyfin.Data.Entities.MusicAlbumMetadata", null) + .WithMany("Labels") + .HasForeignKey("Company_Labels_Id"); + + b.HasOne("Jellyfin.Data.Entities.SeriesMetadata", null) + .WithMany("Networks") + .HasForeignKey("Company_Networks_Id"); + + b.HasOne("Jellyfin.Data.Entities.Company", "Parent") + .WithMany() + .HasForeignKey("Company_Parent_Id"); + + b.HasOne("Jellyfin.Data.Entities.BookMetadata", null) + .WithMany("Publishers") + .HasForeignKey("Company_Publishers_Id"); + + b.HasOne("Jellyfin.Data.Entities.MovieMetadata", null) + .WithMany("Studios") + .HasForeignKey("Company_Studios_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Genre", b => + { + b.HasOne("Jellyfin.Data.Entities.Metadata", null) + .WithMany("Genres") + .HasForeignKey("PersonRole_PersonRoles_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Group", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Groups") + .HasForeignKey("Group_Groups_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.LibraryItem", b => + { + b.HasOne("Jellyfin.Data.Entities.LibraryRoot", "LibraryRoot") + .WithMany() + .HasForeignKey("LibraryRoot_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.LibraryRoot", b => + { + b.HasOne("Jellyfin.Data.Entities.Library", "Library") + .WithMany() + .HasForeignKey("Library_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaFile", b => + { + b.HasOne("Jellyfin.Data.Entities.Release", null) + .WithMany("MediaFiles") + .HasForeignKey("MediaFile_MediaFiles_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaFileStream", b => + { + b.HasOne("Jellyfin.Data.Entities.MediaFile", null) + .WithMany("MediaFileStreams") + .HasForeignKey("MediaFileStream_MediaFileStreams_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MetadataProviderId", b => + { + b.HasOne("Jellyfin.Data.Entities.Person", null) + .WithMany("Sources") + .HasForeignKey("MetadataProviderId_Sources_Id"); + + b.HasOne("Jellyfin.Data.Entities.PersonRole", null) + .WithMany("Sources") + .HasForeignKey("MetadataProviderId_Sources_Id"); + + b.HasOne("Jellyfin.Data.Entities.MetadataProvider", "MetadataProvider") + .WithMany() + .HasForeignKey("MetadataProvider_Id"); + + b.HasOne("Jellyfin.Data.Entities.Metadata", null) + .WithMany("Sources") + .HasForeignKey("PersonRole_PersonRoles_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.Group", null) + .WithMany("GroupPermissions") + .HasForeignKey("Permission_GroupPermissions_Id"); + + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("Permission_Permissions_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PersonRole", b => + { + b.HasOne("Jellyfin.Data.Entities.Artwork", "Artwork") + .WithMany() + .HasForeignKey("Artwork_Artwork_Id"); + + b.HasOne("Jellyfin.Data.Entities.Metadata", null) + .WithMany("PersonRoles") + .HasForeignKey("PersonRole_PersonRoles_Id"); + + b.HasOne("Jellyfin.Data.Entities.Person", "Person") + .WithMany() + .HasForeignKey("Person_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.Group", null) + .WithMany("Preferences") + .HasForeignKey("Preference_Preferences_Id"); + + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("Preference_Preferences_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ProviderMapping", b => + { + b.HasOne("Jellyfin.Data.Entities.Group", null) + .WithMany("ProviderMappings") + .HasForeignKey("ProviderMapping_ProviderMappings_Id"); + + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ProviderMappings") + .HasForeignKey("ProviderMapping_ProviderMappings_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Rating", b => + { + b.HasOne("Jellyfin.Data.Entities.Metadata", null) + .WithMany("Ratings") + .HasForeignKey("PersonRole_PersonRoles_Id"); + + b.HasOne("Jellyfin.Data.Entities.RatingSource", "RatingType") + .WithMany() + .HasForeignKey("RatingSource_RatingType_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.RatingSource", b => + { + b.HasOne("Jellyfin.Data.Entities.MetadataProviderId", "Source") + .WithMany() + .HasForeignKey("MetadataProviderId_Source_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Release", b => + { + b.HasOne("Jellyfin.Data.Entities.Book", null) + .WithMany("Releases") + .HasForeignKey("Release_Releases_Id"); + + b.HasOne("Jellyfin.Data.Entities.CustomItem", null) + .WithMany("Releases") + .HasForeignKey("Release_Releases_Id") + .HasConstraintName("FK_Release_LibraryItem_Release_Releases_Id1"); + + b.HasOne("Jellyfin.Data.Entities.Episode", null) + .WithMany("Releases") + .HasForeignKey("Release_Releases_Id") + .HasConstraintName("FK_Release_LibraryItem_Release_Releases_Id2"); + + b.HasOne("Jellyfin.Data.Entities.Movie", null) + .WithMany("Releases") + .HasForeignKey("Release_Releases_Id") + .HasConstraintName("FK_Release_LibraryItem_Release_Releases_Id3"); + + b.HasOne("Jellyfin.Data.Entities.Photo", null) + .WithMany("Releases") + .HasForeignKey("Release_Releases_Id") + .HasConstraintName("FK_Release_LibraryItem_Release_Releases_Id4"); + + b.HasOne("Jellyfin.Data.Entities.Track", null) + .WithMany("Releases") + .HasForeignKey("Release_Releases_Id") + .HasConstraintName("FK_Release_LibraryItem_Release_Releases_Id5"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Episode", b => + { + b.HasOne("Jellyfin.Data.Entities.Season", null) + .WithMany("Episodes") + .HasForeignKey("Episode_Episodes_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Season", b => + { + b.HasOne("Jellyfin.Data.Entities.Series", null) + .WithMany("Seasons") + .HasForeignKey("Season_Seasons_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Track", b => + { + b.HasOne("Jellyfin.Data.Entities.MusicAlbum", null) + .WithMany("Tracks") + .HasForeignKey("Track_Tracks_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BookMetadata", b => + { + b.HasOne("Jellyfin.Data.Entities.Book", null) + .WithMany("BookMetadata") + .HasForeignKey("BookMetadata_BookMetadata_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CompanyMetadata", b => + { + b.HasOne("Jellyfin.Data.Entities.Company", null) + .WithMany("CompanyMetadata") + .HasForeignKey("CompanyMetadata_CompanyMetadata_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemMetadata", b => + { + b.HasOne("Jellyfin.Data.Entities.CustomItem", null) + .WithMany("CustomItemMetadata") + .HasForeignKey("CustomItemMetadata_CustomItemMetadata_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.EpisodeMetadata", b => + { + b.HasOne("Jellyfin.Data.Entities.Episode", null) + .WithMany("EpisodeMetadata") + .HasForeignKey("EpisodeMetadata_EpisodeMetadata_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MovieMetadata", b => + { + b.HasOne("Jellyfin.Data.Entities.Movie", null) + .WithMany("MovieMetadata") + .HasForeignKey("MovieMetadata_MovieMetadata_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MusicAlbumMetadata", b => + { + b.HasOne("Jellyfin.Data.Entities.MusicAlbum", null) + .WithMany("MusicAlbumMetadata") + .HasForeignKey("MusicAlbumMetadata_MusicAlbumMetadata_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PhotoMetadata", b => + { + b.HasOne("Jellyfin.Data.Entities.Photo", null) + .WithMany("PhotoMetadata") + .HasForeignKey("PhotoMetadata_PhotoMetadata_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.SeasonMetadata", b => + { + b.HasOne("Jellyfin.Data.Entities.Season", null) + .WithMany("SeasonMetadata") + .HasForeignKey("SeasonMetadata_SeasonMetadata_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.SeriesMetadata", b => + { + b.HasOne("Jellyfin.Data.Entities.Series", null) + .WithMany("SeriesMetadata") + .HasForeignKey("SeriesMetadata_SeriesMetadata_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.TrackMetadata", b => + { + b.HasOne("Jellyfin.Data.Entities.Track", null) + .WithMany("TrackMetadata") + .HasForeignKey("TrackMetadata_TrackMetadata_Id"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 88114d999..4194070aa 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -13,6 +13,9 @@ true true enable + + + True @@ -41,6 +44,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index b5ea04dca..82e304586 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -16,7 +16,8 @@ namespace Jellyfin.Server.Migrations internal static readonly IMigrationRoutine[] Migrations = { new Routines.DisableTranscodingThrottling(), - new Routines.CreateUserLoggingConfigFile() + new Routines.CreateUserLoggingConfigFile(), + new Routines.MigrateActivityLogDb() }; /// diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs new file mode 100644 index 000000000..9f1f5b92e --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs @@ -0,0 +1,109 @@ +#pragma warning disable CS1591 + +using System; +using System.IO; +using Emby.Server.Implementations.Data; +using Jellyfin.Data.Entities; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SQLitePCL.pretty; + +namespace Jellyfin.Server.Migrations.Routines +{ + public class MigrateActivityLogDb : IMigrationRoutine + { + private const string DbFilename = "activitylog.db"; + + public Guid Id => Guid.Parse("3793eb59-bc8c-456c-8b9f-bd5a62a42978"); + + public string Name => "MigrateActivityLogDatabase"; + + public void Perform(CoreAppHost host, ILogger logger) + { + var dataPath = host.ServerConfigurationManager.ApplicationPaths.DataPath; + using (var connection = SQLite3.Open( + Path.Combine(dataPath, DbFilename), + ConnectionFlags.ReadOnly, + null)) + { + logger.LogInformation("Migrating the database may take a while, do not stop Jellyfin."); + using var dbContext = host.ServiceProvider.GetService(); + + var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id ASC"); + + // Make sure that the database is empty in case of failed migration due to power outages, etc. + dbContext.ActivityLogs.RemoveRange(dbContext.ActivityLogs); + dbContext.SaveChanges(); + // Reset the autoincrement counter + dbContext.Database.ExecuteSqlRaw("UPDATE sqlite_sequence SET seq = 0 WHERE name = 'ActivityLog';"); + dbContext.SaveChanges(); + + foreach (var entry in queryResult) + { + var newEntry = new ActivityLog( + entry[1].ToString(), + entry[4].ToString(), + entry[6].SQLiteType == SQLiteType.Null ? Guid.Empty : Guid.Parse(entry[6].ToString()), + entry[7].ReadDateTime(), + ParseLogLevel(entry[8].ToString())); + + if (entry[2].SQLiteType != SQLiteType.Null) + { + newEntry.Overview = entry[2].ToString(); + } + + if (entry[3].SQLiteType != SQLiteType.Null) + { + newEntry.ShortOverview = entry[3].ToString(); + } + + if (entry[5].SQLiteType != SQLiteType.Null) + { + newEntry.ItemId = entry[5].ToString(); + } + + dbContext.ActivityLogs.Add(newEntry); + dbContext.SaveChanges(); + } + } + + try + { + File.Move(Path.Combine(dataPath, DbFilename), Path.Combine(dataPath, DbFilename + ".old")); + } + catch (IOException e) + { + logger.LogError(e, "Error renaming legacy activity log database to 'activitylog.db.old'"); + } + } + + private LogLevel ParseLogLevel(string entry) + { + if (string.Equals(entry, "Debug", StringComparison.OrdinalIgnoreCase)) + { + return LogLevel.Debug; + } + + if (string.Equals(entry, "Information", StringComparison.OrdinalIgnoreCase) + || string.Equals(entry, "Info", StringComparison.OrdinalIgnoreCase)) + { + return LogLevel.Information; + } + + if (string.Equals(entry, "Warning", StringComparison.OrdinalIgnoreCase) + || string.Equals(entry, "Warn", StringComparison.OrdinalIgnoreCase)) + { + return LogLevel.Warning; + } + + if (string.Equals(entry, "Error", StringComparison.OrdinalIgnoreCase)) + { + return LogLevel.Error; + } + + return LogLevel.Trace; + } + } +} diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs index a54640b2f..997b1c45a 100644 --- a/MediaBrowser.Api/Library/LibraryService.cs +++ b/MediaBrowser.Api/Library/LibraryService.cs @@ -759,13 +759,14 @@ namespace MediaBrowser.Api.Library { try { - _activityManager.Create(new ActivityLogEntry + _activityManager.Create(new Jellyfin.Data.Entities.ActivityLog( + string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Name, item.Name), + "UserDownloadingContent", + auth.UserId, + DateTime.UtcNow, + LogLevel.Trace) { - Name = string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Name, item.Name), - Type = "UserDownloadingContent", ShortOverview = string.Format(_localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device), - UserId = auth.UserId - }); } catch diff --git a/MediaBrowser.Api/System/ActivityLogService.cs b/MediaBrowser.Api/System/ActivityLogService.cs index f95fa7ca0..0a5fc9433 100644 --- a/MediaBrowser.Api/System/ActivityLogService.cs +++ b/MediaBrowser.Api/System/ActivityLogService.cs @@ -53,7 +53,7 @@ namespace MediaBrowser.Api.System (DateTime?)null : DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); - var result = _activityManager.GetActivityLogEntries(minDate, request.HasUserId, request.StartIndex, request.Limit); + var result = _activityManager.GetPagedResult(request.StartIndex, request.Limit); return ToOptimizedResult(result); } diff --git a/MediaBrowser.Model/Activity/ActivityLogEntry.cs b/MediaBrowser.Model/Activity/ActivityLogEntry.cs index 80f01b66e..5ab904394 100644 --- a/MediaBrowser.Model/Activity/ActivityLogEntry.cs +++ b/MediaBrowser.Model/Activity/ActivityLogEntry.cs @@ -59,6 +59,7 @@ namespace MediaBrowser.Model.Activity /// Gets or sets the user primary image tag. /// /// The user primary image tag. + [Obsolete("UserPrimaryImageTag is not used.")] public string UserPrimaryImageTag { get; set; } /// diff --git a/MediaBrowser.Model/Activity/IActivityManager.cs b/MediaBrowser.Model/Activity/IActivityManager.cs index f336f5272..6742dc8fc 100644 --- a/MediaBrowser.Model/Activity/IActivityManager.cs +++ b/MediaBrowser.Model/Activity/IActivityManager.cs @@ -1,6 +1,10 @@ #pragma warning disable CS1591 using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Model.Events; using MediaBrowser.Model.Querying; @@ -10,10 +14,15 @@ namespace MediaBrowser.Model.Activity { event EventHandler> EntryCreated; - void Create(ActivityLogEntry entry); + void Create(ActivityLog entry); - QueryResult GetActivityLogEntries(DateTime? minDate, int? startIndex, int? limit); + Task CreateAsync(ActivityLog entry); - QueryResult GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? x, int? y); + QueryResult GetPagedResult(int? startIndex, int? limit); + + QueryResult GetPagedResult( + Func, IEnumerable> func, + int? startIndex, + int? limit); } } diff --git a/MediaBrowser.Model/Activity/IActivityRepository.cs b/MediaBrowser.Model/Activity/IActivityRepository.cs deleted file mode 100644 index 66144ec47..000000000 --- a/MediaBrowser.Model/Activity/IActivityRepository.cs +++ /dev/null @@ -1,14 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using MediaBrowser.Model.Querying; - -namespace MediaBrowser.Model.Activity -{ - public interface IActivityRepository - { - void Create(ActivityLogEntry entry); - - QueryResult GetActivityLogEntries(DateTime? minDate, bool? z, int? startIndex, int? limit); - } -} diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index b41d0af1d..5c6e313e0 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -37,6 +37,9 @@ + + + ../jellyfin.ruleset diff --git a/MediaBrowser.sln b/MediaBrowser.sln index a1dbe8047..6d01b0dcd 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26730.3 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30011.22 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Controller", "MediaBrowser.Controller\MediaBrowser.Controller.csproj", "{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}" EndProject @@ -46,23 +46,25 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Drawing.Skia", "Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj", "{154872D9-6C12-4007-96E3-8F70A58386CE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Api", "Jellyfin.Api\Jellyfin.Api.csproj", "{DFBEFB4C-DA19-4143-98B7-27320C7F7163}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Api", "Jellyfin.Api\Jellyfin.Api.csproj", "{DFBEFB4C-DA19-4143-98B7-27320C7F7163}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Common.Tests", "tests\Jellyfin.Common.Tests\Jellyfin.Common.Tests.csproj", "{DF194677-DFD3-42AF-9F75-D44D5A416478}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Common.Tests", "tests\Jellyfin.Common.Tests\Jellyfin.Common.Tests.csproj", "{DF194677-DFD3-42AF-9F75-D44D5A416478}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.MediaEncoding.Tests", "tests\Jellyfin.MediaEncoding.Tests\Jellyfin.MediaEncoding.Tests.csproj", "{28464062-0939-4AA7-9F7B-24DDDA61A7C0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.MediaEncoding.Tests", "tests\Jellyfin.MediaEncoding.Tests\Jellyfin.MediaEncoding.Tests.csproj", "{28464062-0939-4AA7-9F7B-24DDDA61A7C0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Naming.Tests", "tests\Jellyfin.Naming.Tests\Jellyfin.Naming.Tests.csproj", "{3998657B-1CCC-49DD-A19F-275DC8495F57}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Naming.Tests", "tests\Jellyfin.Naming.Tests\Jellyfin.Naming.Tests.csproj", "{3998657B-1CCC-49DD-A19F-275DC8495F57}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Api.Tests", "tests\Jellyfin.Api.Tests\Jellyfin.Api.Tests.csproj", "{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Api.Tests", "tests\Jellyfin.Api.Tests\Jellyfin.Api.Tests.csproj", "{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Implementations.Tests", "tests\Jellyfin.Server.Implementations.Tests\Jellyfin.Server.Implementations.Tests.csproj", "{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementations.Tests", "tests\Jellyfin.Server.Implementations.Tests\Jellyfin.Server.Implementations.Tests.csproj", "{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Controller.Tests", "tests\Jellyfin.Controller.Tests\Jellyfin.Controller.Tests.csproj", "{462584F7-5023-4019-9EAC-B98CA458C0A0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Controller.Tests", "tests\Jellyfin.Controller.Tests\Jellyfin.Controller.Tests.csproj", "{462584F7-5023-4019-9EAC-B98CA458C0A0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Data", "Jellyfin.Data\Jellyfin.Data.csproj", "{F03299F2-469F-40EF-A655-3766F97A5702}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Data", "Jellyfin.Data\Jellyfin.Data.csproj", "{F03299F2-469F-40EF-A655-3766F97A5702}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementations", "Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj", "{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -114,10 +116,6 @@ Global {713F42B5-878E-499D-A878-E4C652B1D5E8}.Debug|Any CPU.Build.0 = Debug|Any CPU {713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|Any CPU.ActiveCfg = Release|Any CPU {713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|Any CPU.Build.0 = Release|Any CPU - {88AE38DF-19D7-406F-A6A9-09527719A21E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {88AE38DF-19D7-406F-A6A9-09527719A21E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {88AE38DF-19D7-406F-A6A9-09527719A21E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {88AE38DF-19D7-406F-A6A9-09527719A21E}.Release|Any CPU.Build.0 = Release|Any CPU {E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.Build.0 = Debug|Any CPU {E383961B-9356-4D5D-8233-9A1079D03055}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -182,10 +180,22 @@ Global {F03299F2-469F-40EF-A655-3766F97A5702}.Debug|Any CPU.Build.0 = Debug|Any CPU {F03299F2-469F-40EF-A655-3766F97A5702}.Release|Any CPU.ActiveCfg = Release|Any CPU {F03299F2-469F-40EF-A655-3766F97A5702}.Release|Any CPU.Build.0 = Release|Any CPU + {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {DF194677-DFD3-42AF-9F75-D44D5A416478} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {28464062-0939-4AA7-9F7B-24DDDA61A7C0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {3998657B-1CCC-49DD-A19F-275DC8495F57} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE} EndGlobalSection @@ -207,12 +217,4 @@ Global $0.DotNetNamingPolicy = $2 $2.DirectoryNamespaceAssociation = PrefixedHierarchical EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {DF194677-DFD3-42AF-9F75-D44D5A416478} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {28464062-0939-4AA7-9F7B-24DDDA61A7C0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {3998657B-1CCC-49DD-A19F-275DC8495F57} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - EndGlobalSection EndGlobal -- cgit v1.2.3 From 8a6ec2fb713cb77e91d2fceea8b4fce8e7d17395 Mon Sep 17 00:00:00 2001 From: gion Date: Wed, 6 May 2020 23:42:53 +0200 Subject: Rename Syncplay to SyncPlay --- Emby.Server.Implementations/ApplicationHost.cs | 6 +- .../Session/SessionManager.cs | 10 +- .../SyncPlay/SyncPlayController.cs | 548 +++++++++++++++++++++ .../SyncPlay/SyncPlayManager.cs | 385 +++++++++++++++ .../Syncplay/SyncplayController.cs | 548 --------------------- .../Syncplay/SyncplayManager.cs | 385 --------------- MediaBrowser.Api/SyncPlay/SyncPlayService.cs | 312 ++++++++++++ MediaBrowser.Api/SyncPlay/TimeSyncService.cs | 52 ++ MediaBrowser.Api/Syncplay/SyncplayService.cs | 312 ------------ MediaBrowser.Api/Syncplay/TimeSyncService.cs | 52 -- MediaBrowser.Controller/Session/ISessionManager.cs | 10 +- MediaBrowser.Controller/SyncPlay/GroupInfo.cs | 150 ++++++ MediaBrowser.Controller/SyncPlay/GroupMember.cs | 28 ++ .../SyncPlay/ISyncPlayController.cs | 67 +++ .../SyncPlay/ISyncPlayManager.cs | 69 +++ MediaBrowser.Controller/Syncplay/GroupInfo.cs | 150 ------ MediaBrowser.Controller/Syncplay/GroupMember.cs | 28 -- .../Syncplay/ISyncplayController.cs | 67 --- .../Syncplay/ISyncplayManager.cs | 69 --- MediaBrowser.Model/Configuration/SyncplayAccess.cs | 6 +- MediaBrowser.Model/SyncPlay/GroupInfoView.cs | 38 ++ MediaBrowser.Model/SyncPlay/GroupUpdate.cs | 26 + MediaBrowser.Model/SyncPlay/GroupUpdateType.cs | 53 ++ MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs | 22 + MediaBrowser.Model/SyncPlay/PlaybackRequest.cs | 34 ++ MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs | 33 ++ MediaBrowser.Model/SyncPlay/SendCommand.cs | 38 ++ MediaBrowser.Model/SyncPlay/SendCommandType.cs | 21 + MediaBrowser.Model/SyncPlay/UtcTimeResponse.cs | 20 + MediaBrowser.Model/Syncplay/GroupInfoView.cs | 38 -- MediaBrowser.Model/Syncplay/GroupUpdate.cs | 26 - MediaBrowser.Model/Syncplay/GroupUpdateType.cs | 53 -- MediaBrowser.Model/Syncplay/JoinGroupRequest.cs | 22 - MediaBrowser.Model/Syncplay/PlaybackRequest.cs | 34 -- MediaBrowser.Model/Syncplay/PlaybackRequestType.cs | 33 -- MediaBrowser.Model/Syncplay/SendCommand.cs | 38 -- MediaBrowser.Model/Syncplay/SendCommandType.cs | 21 - MediaBrowser.Model/Syncplay/UtcTimeResponse.cs | 20 - MediaBrowser.Model/Users/UserPolicy.cs | 8 +- 39 files changed, 1916 insertions(+), 1916 deletions(-) create mode 100644 Emby.Server.Implementations/SyncPlay/SyncPlayController.cs create mode 100644 Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs delete mode 100644 Emby.Server.Implementations/Syncplay/SyncplayController.cs delete mode 100644 Emby.Server.Implementations/Syncplay/SyncplayManager.cs create mode 100644 MediaBrowser.Api/SyncPlay/SyncPlayService.cs create mode 100644 MediaBrowser.Api/SyncPlay/TimeSyncService.cs delete mode 100644 MediaBrowser.Api/Syncplay/SyncplayService.cs delete mode 100644 MediaBrowser.Api/Syncplay/TimeSyncService.cs create mode 100644 MediaBrowser.Controller/SyncPlay/GroupInfo.cs create mode 100644 MediaBrowser.Controller/SyncPlay/GroupMember.cs create mode 100644 MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs create mode 100644 MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs delete mode 100644 MediaBrowser.Controller/Syncplay/GroupInfo.cs delete mode 100644 MediaBrowser.Controller/Syncplay/GroupMember.cs delete mode 100644 MediaBrowser.Controller/Syncplay/ISyncplayController.cs delete mode 100644 MediaBrowser.Controller/Syncplay/ISyncplayManager.cs create mode 100644 MediaBrowser.Model/SyncPlay/GroupInfoView.cs create mode 100644 MediaBrowser.Model/SyncPlay/GroupUpdate.cs create mode 100644 MediaBrowser.Model/SyncPlay/GroupUpdateType.cs create mode 100644 MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs create mode 100644 MediaBrowser.Model/SyncPlay/PlaybackRequest.cs create mode 100644 MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs create mode 100644 MediaBrowser.Model/SyncPlay/SendCommand.cs create mode 100644 MediaBrowser.Model/SyncPlay/SendCommandType.cs create mode 100644 MediaBrowser.Model/SyncPlay/UtcTimeResponse.cs delete mode 100644 MediaBrowser.Model/Syncplay/GroupInfoView.cs delete mode 100644 MediaBrowser.Model/Syncplay/GroupUpdate.cs delete mode 100644 MediaBrowser.Model/Syncplay/GroupUpdateType.cs delete mode 100644 MediaBrowser.Model/Syncplay/JoinGroupRequest.cs delete mode 100644 MediaBrowser.Model/Syncplay/PlaybackRequest.cs delete mode 100644 MediaBrowser.Model/Syncplay/PlaybackRequestType.cs delete mode 100644 MediaBrowser.Model/Syncplay/SendCommand.cs delete mode 100644 MediaBrowser.Model/Syncplay/SendCommandType.cs delete mode 100644 MediaBrowser.Model/Syncplay/UtcTimeResponse.cs (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 8419014c2..730323c22 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -47,7 +47,7 @@ using Emby.Server.Implementations.Session; using Emby.Server.Implementations.SocketSharp; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; -using Emby.Server.Implementations.Syncplay; +using Emby.Server.Implementations.SyncPlay; using MediaBrowser.Api; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; @@ -81,7 +81,7 @@ using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Controller.TV; -using MediaBrowser.Controller.Syncplay; +using MediaBrowser.Controller.SyncPlay; using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.Model.Activity; @@ -645,7 +645,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 6a64209c1..aab745de4 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -25,7 +25,7 @@ using MediaBrowser.Model.Events; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; -using MediaBrowser.Model.Syncplay; +using MediaBrowser.Model.SyncPlay; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Session @@ -1156,19 +1156,19 @@ namespace Emby.Server.Implementations.Session } /// - public async Task SendSyncplayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken) + public async Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken) { CheckDisposed(); var session = GetSessionToRemoteControl(sessionId); - await SendMessageToSession(session, "SyncplayCommand", command, cancellationToken).ConfigureAwait(false); + await SendMessageToSession(session, "SyncPlayCommand", command, cancellationToken).ConfigureAwait(false); } /// - public async Task SendSyncplayGroupUpdate(string sessionId, GroupUpdate command, CancellationToken cancellationToken) + public async Task SendSyncPlayGroupUpdate(string sessionId, GroupUpdate command, CancellationToken cancellationToken) { CheckDisposed(); var session = GetSessionToRemoteControl(sessionId); - await SendMessageToSession(session, "SyncplayGroupUpdate", command, cancellationToken).ConfigureAwait(false); + await SendMessageToSession(session, "SyncPlayGroupUpdate", command, cancellationToken).ConfigureAwait(false); } private IEnumerable TranslateItemForPlayback(Guid id, User user) diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs new file mode 100644 index 000000000..9c9758de1 --- /dev/null +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs @@ -0,0 +1,548 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.SyncPlay; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.SyncPlay; + +namespace Emby.Server.Implementations.SyncPlay +{ + /// + /// Class SyncPlayController. + /// + /// + /// Class is not thread-safe, external locking is required when accessing methods. + /// + public class SyncPlayController : ISyncPlayController, IDisposable + { + /// + /// Used to filter the sessions of a group. + /// + private enum BroadcastType + { + /// + /// All sessions will receive the message. + /// + AllGroup = 0, + /// + /// Only the specified session will receive the message. + /// + CurrentSession = 1, + /// + /// All sessions, except the current one, will receive the message. + /// + AllExceptCurrentSession = 2, + /// + /// Only sessions that are not buffering will receive the message. + /// + AllReady = 3 + } + + /// + /// The session manager. + /// + private readonly ISessionManager _sessionManager; + + /// + /// The SyncPlay manager. + /// + private readonly ISyncPlayManager _syncPlayManager; + + /// + /// The group to manage. + /// + private readonly GroupInfo _group = new GroupInfo(); + + /// + public Guid GetGroupId() => _group.GroupId; + + /// + public Guid GetPlayingItemId() => _group.PlayingItem.Id; + + /// + public bool IsGroupEmpty() => _group.IsEmpty(); + + private bool _disposed = false; + + public SyncPlayController( + ISessionManager sessionManager, + ISyncPlayManager syncPlayManager) + { + _sessionManager = sessionManager; + _syncPlayManager = syncPlayManager; + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and optionally managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + _disposed = true; + } + + // TODO: use this somewhere + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + + /// + /// Converts DateTime to UTC string. + /// + /// The date to convert. + /// The UTC string. + private string DateToUTCString(DateTime date) + { + return date.ToUniversalTime().ToString("o"); + } + + /// + /// Filters sessions of this group. + /// + /// The current session. + /// The filtering type. + /// The array of sessions matching the filter. + private SessionInfo[] FilterSessions(SessionInfo from, BroadcastType type) + { + switch (type) + { + case BroadcastType.CurrentSession: + return new SessionInfo[] { from }; + case BroadcastType.AllGroup: + return _group.Participants.Values.Select( + session => session.Session + ).ToArray(); + case BroadcastType.AllExceptCurrentSession: + return _group.Participants.Values.Select( + session => session.Session + ).Where( + session => !session.Id.Equals(from.Id) + ).ToArray(); + case BroadcastType.AllReady: + return _group.Participants.Values.Where( + session => !session.IsBuffering + ).Select( + session => session.Session + ).ToArray(); + default: + return new SessionInfo[] { }; + } + } + + /// + /// Sends a GroupUpdate message to the interested sessions. + /// + /// The current session. + /// The filtering type. + /// The message to send. + /// The cancellation token. + /// The task. + private Task SendGroupUpdate(SessionInfo from, BroadcastType type, GroupUpdate message, CancellationToken cancellationToken) + { + IEnumerable GetTasks() + { + SessionInfo[] sessions = FilterSessions(from, type); + foreach (var session in sessions) + { + yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), message, cancellationToken); + } + } + + return Task.WhenAll(GetTasks()); + } + + /// + /// Sends a playback command to the interested sessions. + /// + /// The current session. + /// The filtering type. + /// The message to send. + /// The cancellation token. + /// The task. + private Task SendCommand(SessionInfo from, BroadcastType type, SendCommand message, CancellationToken cancellationToken) + { + IEnumerable GetTasks() + { + SessionInfo[] sessions = FilterSessions(from, type); + foreach (var session in sessions) + { + yield return _sessionManager.SendSyncPlayCommand(session.Id.ToString(), message, cancellationToken); + } + } + + return Task.WhenAll(GetTasks()); + } + + /// + /// Builds a new playback command with some default values. + /// + /// The command type. + /// The SendCommand. + private SendCommand NewSyncPlayCommand(SendCommandType type) + { + return new SendCommand() + { + GroupId = _group.GroupId.ToString(), + Command = type, + PositionTicks = _group.PositionTicks, + When = DateToUTCString(_group.LastActivity), + EmittedAt = DateToUTCString(DateTime.UtcNow) + }; + } + + /// + /// Builds a new group update message. + /// + /// The update type. + /// The data to send. + /// The GroupUpdate. + private GroupUpdate NewSyncPlayGroupUpdate(GroupUpdateType type, T data) + { + return new GroupUpdate() + { + GroupId = _group.GroupId.ToString(), + Type = type, + Data = data + }; + } + + /// + public void InitGroup(SessionInfo session, CancellationToken cancellationToken) + { + _group.AddSession(session); + _syncPlayManager.AddSessionToGroup(session, this); + + _group.PlayingItem = session.FullNowPlayingItem; + _group.IsPaused = true; + _group.PositionTicks = session.PlayState.PositionTicks ?? 0; + _group.LastActivity = DateTime.UtcNow; + + var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow)); + SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); + var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause); + SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken); + } + + /// + public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken) + { + if (session.NowPlayingItem?.Id == _group.PlayingItem.Id && request.PlayingItemId == _group.PlayingItem.Id) + { + _group.AddSession(session); + _syncPlayManager.AddSessionToGroup(session, this); + + var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow)); + SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); + + var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName); + SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); + + // Client join and play, syncing will happen client side + if (!_group.IsPaused) + { + var playCommand = NewSyncPlayCommand(SendCommandType.Play); + SendCommand(session, BroadcastType.CurrentSession, playCommand, cancellationToken); + } + else + { + var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause); + SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken); + } + } + else + { + var playRequest = new PlayRequest(); + playRequest.ItemIds = new Guid[] { _group.PlayingItem.Id }; + playRequest.StartPositionTicks = _group.PositionTicks; + var update = NewSyncPlayGroupUpdate(GroupUpdateType.PrepareSession, playRequest); + SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken); + } + } + + /// + public void SessionLeave(SessionInfo session, CancellationToken cancellationToken) + { + _group.RemoveSession(session); + _syncPlayManager.RemoveSessionFromGroup(session, this); + + var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupLeft, _group.PositionTicks); + SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); + + var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserLeft, session.UserName); + SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); + } + + /// + public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) + { + // The server's job is to mantain a consistent state to which clients refer to, + // as also to notify clients of state changes. + // The actual syncing of media playback happens client side. + // Clients are aware of the server's time and use it to sync. + switch (request.Type) + { + case PlaybackRequestType.Play: + HandlePlayRequest(session, request, cancellationToken); + break; + case PlaybackRequestType.Pause: + HandlePauseRequest(session, request, cancellationToken); + break; + case PlaybackRequestType.Seek: + HandleSeekRequest(session, request, cancellationToken); + break; + case PlaybackRequestType.Buffering: + HandleBufferingRequest(session, request, cancellationToken); + break; + case PlaybackRequestType.BufferingDone: + HandleBufferingDoneRequest(session, request, cancellationToken); + break; + case PlaybackRequestType.UpdatePing: + HandlePingUpdateRequest(session, request); + break; + } + } + + /// + /// Handles a play action requested by a session. + /// + /// The session. + /// The play action. + /// The cancellation token. + private void HandlePlayRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) + { + if (_group.IsPaused) + { + // Pick a suitable time that accounts for latency + var delay = _group.GetHighestPing() * 2; + delay = delay < _group.DefaulPing ? _group.DefaulPing : delay; + + // Unpause group and set starting point in future + // Clients will start playback at LastActivity (datetime) from PositionTicks (playback position) + // The added delay does not guarantee, of course, that the command will be received in time + // Playback synchronization will mainly happen client side + _group.IsPaused = false; + _group.LastActivity = DateTime.UtcNow.AddMilliseconds( + delay + ); + + var command = NewSyncPlayCommand(SendCommandType.Play); + SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); + } + else + { + // Client got lost, sending current state + var command = NewSyncPlayCommand(SendCommandType.Play); + SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); + } + } + + /// + /// Handles a pause action requested by a session. + /// + /// The session. + /// The pause action. + /// The cancellation token. + private void HandlePauseRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) + { + if (!_group.IsPaused) + { + // Pause group and compute the media playback position + _group.IsPaused = true; + var currentTime = DateTime.UtcNow; + var elapsedTime = currentTime - _group.LastActivity; + _group.LastActivity = currentTime; + // Seek only if playback actually started + // (a pause request may be issued during the delay added to account for latency) + _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0; + + var command = NewSyncPlayCommand(SendCommandType.Pause); + SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); + } + else + { + // Client got lost, sending current state + var command = NewSyncPlayCommand(SendCommandType.Pause); + SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); + } + } + + /// + /// Handles a seek action requested by a session. + /// + /// The session. + /// The seek action. + /// The cancellation token. + private void HandleSeekRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) + { + // Sanitize PositionTicks + var ticks = SanitizePositionTicks(request.PositionTicks); + + // Pause and seek + _group.IsPaused = true; + _group.PositionTicks = ticks; + _group.LastActivity = DateTime.UtcNow; + + var command = NewSyncPlayCommand(SendCommandType.Seek); + SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); + } + + /// + /// Handles a buffering action requested by a session. + /// + /// The session. + /// The buffering action. + /// The cancellation token. + private void HandleBufferingRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) + { + if (!_group.IsPaused) + { + // Pause group and compute the media playback position + _group.IsPaused = true; + var currentTime = DateTime.UtcNow; + var elapsedTime = currentTime - _group.LastActivity; + _group.LastActivity = currentTime; + _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0; + + _group.SetBuffering(session, true); + + // Send pause command to all non-buffering sessions + var command = NewSyncPlayCommand(SendCommandType.Pause); + SendCommand(session, BroadcastType.AllReady, command, cancellationToken); + + var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.GroupWait, session.UserName); + SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); + } + else + { + // Client got lost, sending current state + var command = NewSyncPlayCommand(SendCommandType.Pause); + SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); + } + } + + /// + /// Handles a buffering-done action requested by a session. + /// + /// The session. + /// The buffering-done action. + /// The cancellation token. + private void HandleBufferingDoneRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) + { + if (_group.IsPaused) + { + _group.SetBuffering(session, false); + + var requestTicks = SanitizePositionTicks(request.PositionTicks); + + var when = request.When ?? DateTime.UtcNow; + var currentTime = DateTime.UtcNow; + var elapsedTime = currentTime - when; + var clientPosition = TimeSpan.FromTicks(requestTicks) + elapsedTime; + var delay = _group.PositionTicks - clientPosition.Ticks; + + if (_group.IsBuffering()) + { + // Others are still buffering, tell this client to pause when ready + var command = NewSyncPlayCommand(SendCommandType.Pause); + var pauseAtTime = currentTime.AddMilliseconds(delay); + command.When = DateToUTCString(pauseAtTime); + SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); + } + else + { + // Let other clients resume as soon as the buffering client catches up + _group.IsPaused = false; + + if (delay > _group.GetHighestPing() * 2) + { + // Client that was buffering is recovering, notifying others to resume + _group.LastActivity = currentTime.AddMilliseconds( + delay + ); + var command = NewSyncPlayCommand(SendCommandType.Play); + SendCommand(session, BroadcastType.AllExceptCurrentSession, command, cancellationToken); + } + else + { + // Client, that was buffering, resumed playback but did not update others in time + delay = _group.GetHighestPing() * 2; + delay = delay < _group.DefaulPing ? _group.DefaulPing : delay; + + _group.LastActivity = currentTime.AddMilliseconds( + delay + ); + + var command = NewSyncPlayCommand(SendCommandType.Play); + SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); + } + } + } + else + { + // Group was not waiting, make sure client has latest state + var command = NewSyncPlayCommand(SendCommandType.Play); + SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); + } + } + + /// + /// Sanitizes the PositionTicks, considers the current playing item when available. + /// + /// The PositionTicks. + /// The sanitized PositionTicks. + private long SanitizePositionTicks(long? positionTicks) + { + var ticks = positionTicks ?? 0; + ticks = ticks >= 0 ? ticks : 0; + if (_group.PlayingItem != null) + { + var runTimeTicks = _group.PlayingItem.RunTimeTicks ?? 0; + ticks = ticks > runTimeTicks ? runTimeTicks : ticks; + } + return ticks; + } + + /// + /// Updates ping of a session. + /// + /// The session. + /// The update. + private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request) + { + // Collected pings are used to account for network latency when unpausing playback + _group.UpdatePing(session, request.Ping ?? _group.DefaulPing); + } + + /// + public GroupInfoView GetInfo() + { + return new GroupInfoView() + { + GroupId = GetGroupId().ToString(), + PlayingItemName = _group.PlayingItem.Name, + PlayingItemId = _group.PlayingItem.Id.ToString(), + PositionTicks = _group.PositionTicks, + Participants = _group.Participants.Values.Select(session => session.Session.UserName).Distinct().ToArray() + }; + } + } +} diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs new file mode 100644 index 000000000..d3197d97b --- /dev/null +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs @@ -0,0 +1,385 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using Microsoft.Extensions.Logging; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.SyncPlay; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.SyncPlay; + +namespace Emby.Server.Implementations.SyncPlay +{ + /// + /// Class SyncPlayManager. + /// + public class SyncPlayManager : ISyncPlayManager, IDisposable + { + /// + /// The logger. + /// + private readonly ILogger _logger; + + /// + /// The user manager. + /// + private readonly IUserManager _userManager; + + /// + /// The session manager. + /// + private readonly ISessionManager _sessionManager; + + /// + /// The library manager. + /// + private readonly ILibraryManager _libraryManager; + + /// + /// The map between sessions and groups. + /// + private readonly Dictionary _sessionToGroupMap = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// The groups. + /// + private readonly Dictionary _groups = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Lock used for accesing any group. + /// + private readonly object _groupsLock = new object(); + + private bool _disposed = false; + + public SyncPlayManager( + ILogger logger, + IUserManager userManager, + ISessionManager sessionManager, + ILibraryManager libraryManager) + { + _logger = logger; + _userManager = userManager; + _sessionManager = sessionManager; + _libraryManager = libraryManager; + + _sessionManager.SessionEnded += OnSessionManagerSessionEnded; + _sessionManager.PlaybackStopped += OnSessionManagerPlaybackStopped; + } + + /// + /// Gets all groups. + /// + /// All groups. + public IEnumerable Groups => _groups.Values; + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and optionally managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + _sessionManager.SessionEnded -= OnSessionManagerSessionEnded; + _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped; + + _disposed = true; + } + + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + + private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e) + { + var session = e.SessionInfo; + if (!IsSessionInGroup(session)) return; + LeaveGroup(session, CancellationToken.None); + } + + private void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e) + { + var session = e.Session; + if (!IsSessionInGroup(session)) return; + LeaveGroup(session, CancellationToken.None); + } + + private bool IsSessionInGroup(SessionInfo session) + { + return _sessionToGroupMap.ContainsKey(session.Id); + } + + private bool HasAccessToItem(User user, Guid itemId) + { + var item = _libraryManager.GetItemById(itemId); + + // Check ParentalRating access + var hasParentalRatingAccess = true; + if (user.Policy.MaxParentalRating.HasValue) + { + hasParentalRatingAccess = item.InheritedParentalRatingValue <= user.Policy.MaxParentalRating; + } + + if (!user.Policy.EnableAllFolders && hasParentalRatingAccess) + { + var collections = _libraryManager.GetCollectionFolders(item).Select( + folder => folder.Id.ToString("N", CultureInfo.InvariantCulture) + ); + var intersect = collections.Intersect(user.Policy.EnabledFolders); + return intersect.Any(); + } + else + { + return hasParentalRatingAccess; + } + } + + private Guid? GetSessionGroup(SessionInfo session) + { + ISyncPlayController group; + _sessionToGroupMap.TryGetValue(session.Id, out group); + if (group != null) + { + return group.GetGroupId(); + } + else + { + return null; + } + } + + /// + public void NewGroup(SessionInfo session, CancellationToken cancellationToken) + { + var user = _userManager.GetUserById(session.UserId); + + if (user.Policy.SyncPlayAccess != SyncPlayAccess.CreateAndJoinGroups) + { + _logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id); + + var error = new GroupUpdate() + { + Type = GroupUpdateType.CreateGroupDenied + }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); + return; + } + + lock (_groupsLock) + { + if (IsSessionInGroup(session)) + { + LeaveGroup(session, cancellationToken); + } + + var group = new SyncPlayController(_sessionManager, this); + _groups[group.GetGroupId().ToString()] = group; + + group.InitGroup(session, cancellationToken); + } + } + + /// + public void JoinGroup(SessionInfo session, string groupId, JoinGroupRequest request, CancellationToken cancellationToken) + { + var user = _userManager.GetUserById(session.UserId); + + if (user.Policy.SyncPlayAccess == SyncPlayAccess.None) + { + _logger.LogWarning("JoinGroup: {0} does not have access to SyncPlay.", session.Id); + + var error = new GroupUpdate() + { + Type = GroupUpdateType.JoinGroupDenied + }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); + return; + } + + lock (_groupsLock) + { + ISyncPlayController group; + _groups.TryGetValue(groupId, out group); + + if (group == null) + { + _logger.LogWarning("JoinGroup: {0} tried to join group {0} that does not exist.", session.Id, groupId); + + var error = new GroupUpdate() + { + Type = GroupUpdateType.GroupDoesNotExist + }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); + return; + } + + if (!HasAccessToItem(user, group.GetPlayingItemId())) + { + _logger.LogWarning("JoinGroup: {0} does not have access to {1}.", session.Id, group.GetPlayingItemId()); + + var error = new GroupUpdate() + { + GroupId = group.GetGroupId().ToString(), + Type = GroupUpdateType.LibraryAccessDenied + }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); + return; + } + + if (IsSessionInGroup(session)) + { + if (GetSessionGroup(session).Equals(groupId)) return; + LeaveGroup(session, cancellationToken); + } + + group.SessionJoin(session, request, cancellationToken); + } + } + + /// + public void LeaveGroup(SessionInfo session, CancellationToken cancellationToken) + { + // TODO: determine what happens to users that are in a group and get their permissions revoked + lock (_groupsLock) + { + ISyncPlayController group; + _sessionToGroupMap.TryGetValue(session.Id, out group); + + if (group == null) + { + _logger.LogWarning("LeaveGroup: {0} does not belong to any group.", session.Id); + + var error = new GroupUpdate() + { + Type = GroupUpdateType.NotInGroup + }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); + return; + } + + group.SessionLeave(session, cancellationToken); + + if (group.IsGroupEmpty()) + { + _logger.LogInformation("LeaveGroup: removing empty group {0}.", group.GetGroupId()); + _groups.Remove(group.GetGroupId().ToString(), out _); + } + } + } + + /// + public List ListGroups(SessionInfo session, Guid filterItemId) + { + var user = _userManager.GetUserById(session.UserId); + + if (user.Policy.SyncPlayAccess == SyncPlayAccess.None) + { + return new List(); + } + + // Filter by item if requested + if (!filterItemId.Equals(Guid.Empty)) + { + return _groups.Values.Where( + group => group.GetPlayingItemId().Equals(filterItemId) && HasAccessToItem(user, group.GetPlayingItemId()) + ).Select( + group => group.GetInfo() + ).ToList(); + } + // Otherwise show all available groups + else + { + return _groups.Values.Where( + group => HasAccessToItem(user, group.GetPlayingItemId()) + ).Select( + group => group.GetInfo() + ).ToList(); + } + } + + /// + public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) + { + var user = _userManager.GetUserById(session.UserId); + + if (user.Policy.SyncPlayAccess == SyncPlayAccess.None) + { + _logger.LogWarning("HandleRequest: {0} does not have access to SyncPlay.", session.Id); + + var error = new GroupUpdate() + { + Type = GroupUpdateType.JoinGroupDenied + }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); + return; + } + + lock (_groupsLock) + { + ISyncPlayController group; + _sessionToGroupMap.TryGetValue(session.Id, out group); + + if (group == null) + { + _logger.LogWarning("HandleRequest: {0} does not belong to any group.", session.Id); + + var error = new GroupUpdate() + { + Type = GroupUpdateType.NotInGroup + }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); + return; + } + + group.HandleRequest(session, request, cancellationToken); + } + } + + /// + public void AddSessionToGroup(SessionInfo session, ISyncPlayController group) + { + if (IsSessionInGroup(session)) + { + throw new InvalidOperationException("Session in other group already!"); + } + _sessionToGroupMap[session.Id] = group; + } + + /// + public void RemoveSessionFromGroup(SessionInfo session, ISyncPlayController group) + { + if (!IsSessionInGroup(session)) + { + throw new InvalidOperationException("Session not in any group!"); + } + + ISyncPlayController tempGroup; + _sessionToGroupMap.Remove(session.Id, out tempGroup); + + if (!tempGroup.GetGroupId().Equals(group.GetGroupId())) + { + throw new InvalidOperationException("Session was in wrong group!"); + } + } + } +} diff --git a/Emby.Server.Implementations/Syncplay/SyncplayController.cs b/Emby.Server.Implementations/Syncplay/SyncplayController.cs deleted file mode 100644 index 8cc3d1fac..000000000 --- a/Emby.Server.Implementations/Syncplay/SyncplayController.cs +++ /dev/null @@ -1,548 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Session; -using MediaBrowser.Controller.Syncplay; -using MediaBrowser.Model.Session; -using MediaBrowser.Model.Syncplay; - -namespace Emby.Server.Implementations.Syncplay -{ - /// - /// Class SyncplayController. - /// - /// - /// Class is not thread-safe, external locking is required when accessing methods. - /// - public class SyncplayController : ISyncplayController, IDisposable - { - /// - /// Used to filter the sessions of a group. - /// - private enum BroadcastType - { - /// - /// All sessions will receive the message. - /// - AllGroup = 0, - /// - /// Only the specified session will receive the message. - /// - CurrentSession = 1, - /// - /// All sessions, except the current one, will receive the message. - /// - AllExceptCurrentSession = 2, - /// - /// Only sessions that are not buffering will receive the message. - /// - AllReady = 3 - } - - /// - /// The session manager. - /// - private readonly ISessionManager _sessionManager; - - /// - /// The syncplay manager. - /// - private readonly ISyncplayManager _syncplayManager; - - /// - /// The group to manage. - /// - private readonly GroupInfo _group = new GroupInfo(); - - /// - public Guid GetGroupId() => _group.GroupId; - - /// - public Guid GetPlayingItemId() => _group.PlayingItem.Id; - - /// - public bool IsGroupEmpty() => _group.IsEmpty(); - - private bool _disposed = false; - - public SyncplayController( - ISessionManager sessionManager, - ISyncplayManager syncplayManager) - { - _sessionManager = sessionManager; - _syncplayManager = syncplayManager; - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases unmanaged and optionally managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - _disposed = true; - } - - // TODO: use this somewhere - private void CheckDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } - } - - /// - /// Converts DateTime to UTC string. - /// - /// The date to convert. - /// The UTC string. - private string DateToUTCString(DateTime date) - { - return date.ToUniversalTime().ToString("o"); - } - - /// - /// Filters sessions of this group. - /// - /// The current session. - /// The filtering type. - /// The array of sessions matching the filter. - private SessionInfo[] FilterSessions(SessionInfo from, BroadcastType type) - { - switch (type) - { - case BroadcastType.CurrentSession: - return new SessionInfo[] { from }; - case BroadcastType.AllGroup: - return _group.Participants.Values.Select( - session => session.Session - ).ToArray(); - case BroadcastType.AllExceptCurrentSession: - return _group.Participants.Values.Select( - session => session.Session - ).Where( - session => !session.Id.Equals(from.Id) - ).ToArray(); - case BroadcastType.AllReady: - return _group.Participants.Values.Where( - session => !session.IsBuffering - ).Select( - session => session.Session - ).ToArray(); - default: - return new SessionInfo[] { }; - } - } - - /// - /// Sends a GroupUpdate message to the interested sessions. - /// - /// The current session. - /// The filtering type. - /// The message to send. - /// The cancellation token. - /// The task. - private Task SendGroupUpdate(SessionInfo from, BroadcastType type, GroupUpdate message, CancellationToken cancellationToken) - { - IEnumerable GetTasks() - { - SessionInfo[] sessions = FilterSessions(from, type); - foreach (var session in sessions) - { - yield return _sessionManager.SendSyncplayGroupUpdate(session.Id.ToString(), message, cancellationToken); - } - } - - return Task.WhenAll(GetTasks()); - } - - /// - /// Sends a playback command to the interested sessions. - /// - /// The current session. - /// The filtering type. - /// The message to send. - /// The cancellation token. - /// The task. - private Task SendCommand(SessionInfo from, BroadcastType type, SendCommand message, CancellationToken cancellationToken) - { - IEnumerable GetTasks() - { - SessionInfo[] sessions = FilterSessions(from, type); - foreach (var session in sessions) - { - yield return _sessionManager.SendSyncplayCommand(session.Id.ToString(), message, cancellationToken); - } - } - - return Task.WhenAll(GetTasks()); - } - - /// - /// Builds a new playback command with some default values. - /// - /// The command type. - /// The SendCommand. - private SendCommand NewSyncplayCommand(SendCommandType type) - { - return new SendCommand() - { - GroupId = _group.GroupId.ToString(), - Command = type, - PositionTicks = _group.PositionTicks, - When = DateToUTCString(_group.LastActivity), - EmittedAt = DateToUTCString(DateTime.UtcNow) - }; - } - - /// - /// Builds a new group update message. - /// - /// The update type. - /// The data to send. - /// The GroupUpdate. - private GroupUpdate NewSyncplayGroupUpdate(GroupUpdateType type, T data) - { - return new GroupUpdate() - { - GroupId = _group.GroupId.ToString(), - Type = type, - Data = data - }; - } - - /// - public void InitGroup(SessionInfo session, CancellationToken cancellationToken) - { - _group.AddSession(session); - _syncplayManager.AddSessionToGroup(session, this); - - _group.PlayingItem = session.FullNowPlayingItem; - _group.IsPaused = true; - _group.PositionTicks = session.PlayState.PositionTicks ?? 0; - _group.LastActivity = DateTime.UtcNow; - - var updateSession = NewSyncplayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow)); - SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); - var pauseCommand = NewSyncplayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken); - } - - /// - public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken) - { - if (session.NowPlayingItem?.Id == _group.PlayingItem.Id && request.PlayingItemId == _group.PlayingItem.Id) - { - _group.AddSession(session); - _syncplayManager.AddSessionToGroup(session, this); - - var updateSession = NewSyncplayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow)); - SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); - - var updateOthers = NewSyncplayGroupUpdate(GroupUpdateType.UserJoined, session.UserName); - SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); - - // Client join and play, syncing will happen client side - if (!_group.IsPaused) - { - var playCommand = NewSyncplayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.CurrentSession, playCommand, cancellationToken); - } - else - { - var pauseCommand = NewSyncplayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken); - } - } - else - { - var playRequest = new PlayRequest(); - playRequest.ItemIds = new Guid[] { _group.PlayingItem.Id }; - playRequest.StartPositionTicks = _group.PositionTicks; - var update = NewSyncplayGroupUpdate(GroupUpdateType.PrepareSession, playRequest); - SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken); - } - } - - /// - public void SessionLeave(SessionInfo session, CancellationToken cancellationToken) - { - _group.RemoveSession(session); - _syncplayManager.RemoveSessionFromGroup(session, this); - - var updateSession = NewSyncplayGroupUpdate(GroupUpdateType.GroupLeft, _group.PositionTicks); - SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); - - var updateOthers = NewSyncplayGroupUpdate(GroupUpdateType.UserLeft, session.UserName); - SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); - } - - /// - public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - // The server's job is to mantain a consistent state to which clients refer to, - // as also to notify clients of state changes. - // The actual syncing of media playback happens client side. - // Clients are aware of the server's time and use it to sync. - switch (request.Type) - { - case PlaybackRequestType.Play: - HandlePlayRequest(session, request, cancellationToken); - break; - case PlaybackRequestType.Pause: - HandlePauseRequest(session, request, cancellationToken); - break; - case PlaybackRequestType.Seek: - HandleSeekRequest(session, request, cancellationToken); - break; - case PlaybackRequestType.Buffering: - HandleBufferingRequest(session, request, cancellationToken); - break; - case PlaybackRequestType.BufferingDone: - HandleBufferingDoneRequest(session, request, cancellationToken); - break; - case PlaybackRequestType.UpdatePing: - HandlePingUpdateRequest(session, request); - break; - } - } - - /// - /// Handles a play action requested by a session. - /// - /// The session. - /// The play action. - /// The cancellation token. - private void HandlePlayRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - if (_group.IsPaused) - { - // Pick a suitable time that accounts for latency - var delay = _group.GetHighestPing() * 2; - delay = delay < _group.DefaulPing ? _group.DefaulPing : delay; - - // Unpause group and set starting point in future - // Clients will start playback at LastActivity (datetime) from PositionTicks (playback position) - // The added delay does not guarantee, of course, that the command will be received in time - // Playback synchronization will mainly happen client side - _group.IsPaused = false; - _group.LastActivity = DateTime.UtcNow.AddMilliseconds( - delay - ); - - var command = NewSyncplayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); - } - else - { - // Client got lost, sending current state - var command = NewSyncplayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); - } - } - - /// - /// Handles a pause action requested by a session. - /// - /// The session. - /// The pause action. - /// The cancellation token. - private void HandlePauseRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - if (!_group.IsPaused) - { - // Pause group and compute the media playback position - _group.IsPaused = true; - var currentTime = DateTime.UtcNow; - var elapsedTime = currentTime - _group.LastActivity; - _group.LastActivity = currentTime; - // Seek only if playback actually started - // (a pause request may be issued during the delay added to account for latency) - _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0; - - var command = NewSyncplayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); - } - else - { - // Client got lost, sending current state - var command = NewSyncplayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); - } - } - - /// - /// Handles a seek action requested by a session. - /// - /// The session. - /// The seek action. - /// The cancellation token. - private void HandleSeekRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - // Sanitize PositionTicks - var ticks = SanitizePositionTicks(request.PositionTicks); - - // Pause and seek - _group.IsPaused = true; - _group.PositionTicks = ticks; - _group.LastActivity = DateTime.UtcNow; - - var command = NewSyncplayCommand(SendCommandType.Seek); - SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); - } - - /// - /// Handles a buffering action requested by a session. - /// - /// The session. - /// The buffering action. - /// The cancellation token. - private void HandleBufferingRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - if (!_group.IsPaused) - { - // Pause group and compute the media playback position - _group.IsPaused = true; - var currentTime = DateTime.UtcNow; - var elapsedTime = currentTime - _group.LastActivity; - _group.LastActivity = currentTime; - _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0; - - _group.SetBuffering(session, true); - - // Send pause command to all non-buffering sessions - var command = NewSyncplayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.AllReady, command, cancellationToken); - - var updateOthers = NewSyncplayGroupUpdate(GroupUpdateType.GroupWait, session.UserName); - SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); - } - else - { - // Client got lost, sending current state - var command = NewSyncplayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); - } - } - - /// - /// Handles a buffering-done action requested by a session. - /// - /// The session. - /// The buffering-done action. - /// The cancellation token. - private void HandleBufferingDoneRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - if (_group.IsPaused) - { - _group.SetBuffering(session, false); - - var requestTicks = SanitizePositionTicks(request.PositionTicks); - - var when = request.When ?? DateTime.UtcNow; - var currentTime = DateTime.UtcNow; - var elapsedTime = currentTime - when; - var clientPosition = TimeSpan.FromTicks(requestTicks) + elapsedTime; - var delay = _group.PositionTicks - clientPosition.Ticks; - - if (_group.IsBuffering()) - { - // Others are still buffering, tell this client to pause when ready - var command = NewSyncplayCommand(SendCommandType.Pause); - var pauseAtTime = currentTime.AddMilliseconds(delay); - command.When = DateToUTCString(pauseAtTime); - SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); - } - else - { - // Let other clients resume as soon as the buffering client catches up - _group.IsPaused = false; - - if (delay > _group.GetHighestPing() * 2) - { - // Client that was buffering is recovering, notifying others to resume - _group.LastActivity = currentTime.AddMilliseconds( - delay - ); - var command = NewSyncplayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.AllExceptCurrentSession, command, cancellationToken); - } - else - { - // Client, that was buffering, resumed playback but did not update others in time - delay = _group.GetHighestPing() * 2; - delay = delay < _group.DefaulPing ? _group.DefaulPing : delay; - - _group.LastActivity = currentTime.AddMilliseconds( - delay - ); - - var command = NewSyncplayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); - } - } - } - else - { - // Group was not waiting, make sure client has latest state - var command = NewSyncplayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); - } - } - - /// - /// Sanitizes the PositionTicks, considers the current playing item when available. - /// - /// The PositionTicks. - /// The sanitized PositionTicks. - private long SanitizePositionTicks(long? positionTicks) - { - var ticks = positionTicks ?? 0; - ticks = ticks >= 0 ? ticks : 0; - if (_group.PlayingItem != null) - { - var runTimeTicks = _group.PlayingItem.RunTimeTicks ?? 0; - ticks = ticks > runTimeTicks ? runTimeTicks : ticks; - } - return ticks; - } - - /// - /// Updates ping of a session. - /// - /// The session. - /// The update. - private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request) - { - // Collected pings are used to account for network latency when unpausing playback - _group.UpdatePing(session, request.Ping ?? _group.DefaulPing); - } - - /// - public GroupInfoView GetInfo() - { - return new GroupInfoView() - { - GroupId = GetGroupId().ToString(), - PlayingItemName = _group.PlayingItem.Name, - PlayingItemId = _group.PlayingItem.Id.ToString(), - PositionTicks = _group.PositionTicks, - Participants = _group.Participants.Values.Select(session => session.Session.UserName).Distinct().ToArray() - }; - } - } -} diff --git a/Emby.Server.Implementations/Syncplay/SyncplayManager.cs b/Emby.Server.Implementations/Syncplay/SyncplayManager.cs deleted file mode 100644 index 7074e2225..000000000 --- a/Emby.Server.Implementations/Syncplay/SyncplayManager.cs +++ /dev/null @@ -1,385 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using Microsoft.Extensions.Logging; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Session; -using MediaBrowser.Controller.Syncplay; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Syncplay; - -namespace Emby.Server.Implementations.Syncplay -{ - /// - /// Class SyncplayManager. - /// - public class SyncplayManager : ISyncplayManager, IDisposable - { - /// - /// The logger. - /// - private readonly ILogger _logger; - - /// - /// The user manager. - /// - private readonly IUserManager _userManager; - - /// - /// The session manager. - /// - private readonly ISessionManager _sessionManager; - - /// - /// The library manager. - /// - private readonly ILibraryManager _libraryManager; - - /// - /// The map between sessions and groups. - /// - private readonly Dictionary _sessionToGroupMap = - new Dictionary(StringComparer.OrdinalIgnoreCase); - - /// - /// The groups. - /// - private readonly Dictionary _groups = - new Dictionary(StringComparer.OrdinalIgnoreCase); - - /// - /// Lock used for accesing any group. - /// - private readonly object _groupsLock = new object(); - - private bool _disposed = false; - - public SyncplayManager( - ILogger logger, - IUserManager userManager, - ISessionManager sessionManager, - ILibraryManager libraryManager) - { - _logger = logger; - _userManager = userManager; - _sessionManager = sessionManager; - _libraryManager = libraryManager; - - _sessionManager.SessionEnded += OnSessionManagerSessionEnded; - _sessionManager.PlaybackStopped += OnSessionManagerPlaybackStopped; - } - - /// - /// Gets all groups. - /// - /// All groups. - public IEnumerable Groups => _groups.Values; - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases unmanaged and optionally managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - _sessionManager.SessionEnded -= OnSessionManagerSessionEnded; - _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped; - - _disposed = true; - } - - private void CheckDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } - } - - private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e) - { - var session = e.SessionInfo; - if (!IsSessionInGroup(session)) return; - LeaveGroup(session, CancellationToken.None); - } - - private void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e) - { - var session = e.Session; - if (!IsSessionInGroup(session)) return; - LeaveGroup(session, CancellationToken.None); - } - - private bool IsSessionInGroup(SessionInfo session) - { - return _sessionToGroupMap.ContainsKey(session.Id); - } - - private bool HasAccessToItem(User user, Guid itemId) - { - var item = _libraryManager.GetItemById(itemId); - - // Check ParentalRating access - var hasParentalRatingAccess = true; - if (user.Policy.MaxParentalRating.HasValue) - { - hasParentalRatingAccess = item.InheritedParentalRatingValue <= user.Policy.MaxParentalRating; - } - - if (!user.Policy.EnableAllFolders && hasParentalRatingAccess) - { - var collections = _libraryManager.GetCollectionFolders(item).Select( - folder => folder.Id.ToString("N", CultureInfo.InvariantCulture) - ); - var intersect = collections.Intersect(user.Policy.EnabledFolders); - return intersect.Any(); - } - else - { - return hasParentalRatingAccess; - } - } - - private Guid? GetSessionGroup(SessionInfo session) - { - ISyncplayController group; - _sessionToGroupMap.TryGetValue(session.Id, out group); - if (group != null) - { - return group.GetGroupId(); - } - else - { - return null; - } - } - - /// - public void NewGroup(SessionInfo session, CancellationToken cancellationToken) - { - var user = _userManager.GetUserById(session.UserId); - - if (user.Policy.SyncplayAccess != SyncplayAccess.CreateAndJoinGroups) - { - _logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id); - - var error = new GroupUpdate() - { - Type = GroupUpdateType.CreateGroupDenied - }; - _sessionManager.SendSyncplayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); - return; - } - - lock (_groupsLock) - { - if (IsSessionInGroup(session)) - { - LeaveGroup(session, cancellationToken); - } - - var group = new SyncplayController(_sessionManager, this); - _groups[group.GetGroupId().ToString()] = group; - - group.InitGroup(session, cancellationToken); - } - } - - /// - public void JoinGroup(SessionInfo session, string groupId, JoinGroupRequest request, CancellationToken cancellationToken) - { - var user = _userManager.GetUserById(session.UserId); - - if (user.Policy.SyncplayAccess == SyncplayAccess.None) - { - _logger.LogWarning("JoinGroup: {0} does not have access to Syncplay.", session.Id); - - var error = new GroupUpdate() - { - Type = GroupUpdateType.JoinGroupDenied - }; - _sessionManager.SendSyncplayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); - return; - } - - lock (_groupsLock) - { - ISyncplayController group; - _groups.TryGetValue(groupId, out group); - - if (group == null) - { - _logger.LogWarning("JoinGroup: {0} tried to join group {0} that does not exist.", session.Id, groupId); - - var error = new GroupUpdate() - { - Type = GroupUpdateType.GroupDoesNotExist - }; - _sessionManager.SendSyncplayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); - return; - } - - if (!HasAccessToItem(user, group.GetPlayingItemId())) - { - _logger.LogWarning("JoinGroup: {0} does not have access to {1}.", session.Id, group.GetPlayingItemId()); - - var error = new GroupUpdate() - { - GroupId = group.GetGroupId().ToString(), - Type = GroupUpdateType.LibraryAccessDenied - }; - _sessionManager.SendSyncplayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); - return; - } - - if (IsSessionInGroup(session)) - { - if (GetSessionGroup(session).Equals(groupId)) return; - LeaveGroup(session, cancellationToken); - } - - group.SessionJoin(session, request, cancellationToken); - } - } - - /// - public void LeaveGroup(SessionInfo session, CancellationToken cancellationToken) - { - // TODO: determine what happens to users that are in a group and get their permissions revoked - lock (_groupsLock) - { - ISyncplayController group; - _sessionToGroupMap.TryGetValue(session.Id, out group); - - if (group == null) - { - _logger.LogWarning("LeaveGroup: {0} does not belong to any group.", session.Id); - - var error = new GroupUpdate() - { - Type = GroupUpdateType.NotInGroup - }; - _sessionManager.SendSyncplayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); - return; - } - - group.SessionLeave(session, cancellationToken); - - if (group.IsGroupEmpty()) - { - _logger.LogInformation("LeaveGroup: removing empty group {0}.", group.GetGroupId()); - _groups.Remove(group.GetGroupId().ToString(), out _); - } - } - } - - /// - public List ListGroups(SessionInfo session, Guid filterItemId) - { - var user = _userManager.GetUserById(session.UserId); - - if (user.Policy.SyncplayAccess == SyncplayAccess.None) - { - return new List(); - } - - // Filter by item if requested - if (!filterItemId.Equals(Guid.Empty)) - { - return _groups.Values.Where( - group => group.GetPlayingItemId().Equals(filterItemId) && HasAccessToItem(user, group.GetPlayingItemId()) - ).Select( - group => group.GetInfo() - ).ToList(); - } - // Otherwise show all available groups - else - { - return _groups.Values.Where( - group => HasAccessToItem(user, group.GetPlayingItemId()) - ).Select( - group => group.GetInfo() - ).ToList(); - } - } - - /// - public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - var user = _userManager.GetUserById(session.UserId); - - if (user.Policy.SyncplayAccess == SyncplayAccess.None) - { - _logger.LogWarning("HandleRequest: {0} does not have access to Syncplay.", session.Id); - - var error = new GroupUpdate() - { - Type = GroupUpdateType.JoinGroupDenied - }; - _sessionManager.SendSyncplayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); - return; - } - - lock (_groupsLock) - { - ISyncplayController group; - _sessionToGroupMap.TryGetValue(session.Id, out group); - - if (group == null) - { - _logger.LogWarning("HandleRequest: {0} does not belong to any group.", session.Id); - - var error = new GroupUpdate() - { - Type = GroupUpdateType.NotInGroup - }; - _sessionManager.SendSyncplayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); - return; - } - - group.HandleRequest(session, request, cancellationToken); - } - } - - /// - public void AddSessionToGroup(SessionInfo session, ISyncplayController group) - { - if (IsSessionInGroup(session)) - { - throw new InvalidOperationException("Session in other group already!"); - } - _sessionToGroupMap[session.Id] = group; - } - - /// - public void RemoveSessionFromGroup(SessionInfo session, ISyncplayController group) - { - if (!IsSessionInGroup(session)) - { - throw new InvalidOperationException("Session not in any group!"); - } - - ISyncplayController tempGroup; - _sessionToGroupMap.Remove(session.Id, out tempGroup); - - if (!tempGroup.GetGroupId().Equals(group.GetGroupId())) - { - throw new InvalidOperationException("Session was in wrong group!"); - } - } - } -} diff --git a/MediaBrowser.Api/SyncPlay/SyncPlayService.cs b/MediaBrowser.Api/SyncPlay/SyncPlayService.cs new file mode 100644 index 000000000..bcdc833e4 --- /dev/null +++ b/MediaBrowser.Api/SyncPlay/SyncPlayService.cs @@ -0,0 +1,312 @@ +using System.Threading; +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.SyncPlay; +using MediaBrowser.Model.Services; +using MediaBrowser.Model.SyncPlay; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Api.SyncPlay +{ + [Route("/SyncPlay/{SessionId}/NewGroup", "POST", Summary = "Create a new SyncPlay group")] + [Authenticated] + public class SyncPlayNewGroup : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + } + + [Route("/SyncPlay/{SessionId}/JoinGroup", "POST", Summary = "Join an existing SyncPlay group")] + [Authenticated] + public class SyncPlayJoinGroup : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + + /// + /// Gets or sets the Group id. + /// + /// The Group id to join. + [ApiMember(Name = "GroupId", Description = "Group Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string GroupId { get; set; } + + /// + /// Gets or sets the playing item id. + /// + /// The client's currently playing item id. + [ApiMember(Name = "PlayingItemId", Description = "Client's playing item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string PlayingItemId { get; set; } + } + + [Route("/SyncPlay/{SessionId}/LeaveGroup", "POST", Summary = "Leave joined SyncPlay group")] + [Authenticated] + public class SyncPlayLeaveGroup : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + } + + [Route("/SyncPlay/{SessionId}/ListGroups", "POST", Summary = "List SyncPlay groups")] + [Authenticated] + public class SyncPlayListGroups : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + + /// + /// Gets or sets the filter item id. + /// + /// The filter item id. + [ApiMember(Name = "FilterItemId", Description = "Filter by item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string FilterItemId { get; set; } + } + + [Route("/SyncPlay/{SessionId}/PlayRequest", "POST", Summary = "Request play in SyncPlay group")] + [Authenticated] + public class SyncPlayPlayRequest : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + } + + [Route("/SyncPlay/{SessionId}/PauseRequest", "POST", Summary = "Request pause in SyncPlay group")] + [Authenticated] + public class SyncPlayPauseRequest : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + } + + [Route("/SyncPlay/{SessionId}/SeekRequest", "POST", Summary = "Request seek in SyncPlay group")] + [Authenticated] + public class SyncPlaySeekRequest : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + + [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")] + public long PositionTicks { get; set; } + } + + [Route("/SyncPlay/{SessionId}/BufferingRequest", "POST", Summary = "Request group wait in SyncPlay group while buffering")] + [Authenticated] + public class SyncPlayBufferingRequest : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + + /// + /// Gets or sets the date used to pin PositionTicks in time. + /// + /// The date related to PositionTicks. + [ApiMember(Name = "When", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string When { get; set; } + + [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")] + public long PositionTicks { get; set; } + + /// + /// Gets or sets whether this is a buffering or a buffering-done request. + /// + /// true if buffering is complete; false otherwise. + [ApiMember(Name = "BufferingDone", IsRequired = true, DataType = "bool", ParameterType = "query", Verb = "POST")] + public bool BufferingDone { get; set; } + } + + [Route("/SyncPlay/{SessionId}/UpdatePing", "POST", Summary = "Update session ping")] + [Authenticated] + public class SyncPlayUpdatePing : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + + [ApiMember(Name = "Ping", IsRequired = true, DataType = "double", ParameterType = "query", Verb = "POST")] + public double Ping { get; set; } + } + + /// + /// Class SyncPlayService. + /// + public class SyncPlayService : BaseApiService + { + /// + /// The session context. + /// + private readonly ISessionContext _sessionContext; + + /// + /// The SyncPlay manager. + /// + private readonly ISyncPlayManager _syncPlayManager; + + public SyncPlayService( + ILogger logger, + IServerConfigurationManager serverConfigurationManager, + IHttpResultFactory httpResultFactory, + ISessionContext sessionContext, + ISyncPlayManager syncPlayManager) + : base(logger, serverConfigurationManager, httpResultFactory) + { + _sessionContext = sessionContext; + _syncPlayManager = syncPlayManager; + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncPlayNewGroup request) + { + var currentSession = GetSession(_sessionContext); + _syncPlayManager.NewGroup(currentSession, CancellationToken.None); + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncPlayJoinGroup request) + { + var currentSession = GetSession(_sessionContext); + var joinRequest = new JoinGroupRequest() + { + GroupId = Guid.Parse(request.GroupId) + }; + + // Both null and empty strings mean that client isn't playing anything + if (!String.IsNullOrEmpty(request.PlayingItemId)) + { + try + { + joinRequest.PlayingItemId = Guid.Parse(request.PlayingItemId); + } + catch (ArgumentNullException) + { + // Should never happen, but just in case + Logger.LogError("JoinGroup: null value for PlayingItemId. Ignoring request."); + return; + } + catch (FormatException) + { + Logger.LogError("JoinGroup: {0} is not a valid format for PlayingItemId. Ignoring request.", request.PlayingItemId); + return; + } + } + _syncPlayManager.JoinGroup(currentSession, request.GroupId, joinRequest, CancellationToken.None); + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncPlayLeaveGroup request) + { + var currentSession = GetSession(_sessionContext); + _syncPlayManager.LeaveGroup(currentSession, CancellationToken.None); + } + + /// + /// Handles the specified request. + /// + /// The request. + /// The requested list of groups. + public List Post(SyncPlayListGroups request) + { + var currentSession = GetSession(_sessionContext); + var filterItemId = Guid.Empty; + if (!String.IsNullOrEmpty(request.FilterItemId)) + { + try + { + filterItemId = Guid.Parse(request.FilterItemId); + } + catch (ArgumentNullException) + { + Logger.LogWarning("ListGroups: null value for FilterItemId. Ignoring filter."); + } + catch (FormatException) + { + Logger.LogWarning("ListGroups: {0} is not a valid format for FilterItemId. Ignoring filter.", request.FilterItemId); + } + } + return _syncPlayManager.ListGroups(currentSession, filterItemId); + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncPlayPlayRequest request) + { + var currentSession = GetSession(_sessionContext); + var syncPlayRequest = new PlaybackRequest() + { + Type = PlaybackRequestType.Play + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncPlayPauseRequest request) + { + var currentSession = GetSession(_sessionContext); + var syncPlayRequest = new PlaybackRequest() + { + Type = PlaybackRequestType.Pause + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncPlaySeekRequest request) + { + var currentSession = GetSession(_sessionContext); + var syncPlayRequest = new PlaybackRequest() + { + Type = PlaybackRequestType.Seek, + PositionTicks = request.PositionTicks + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncPlayBufferingRequest request) + { + var currentSession = GetSession(_sessionContext); + var syncPlayRequest = new PlaybackRequest() + { + Type = request.BufferingDone ? PlaybackRequestType.BufferingDone : PlaybackRequestType.Buffering, + When = DateTime.Parse(request.When), + PositionTicks = request.PositionTicks + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncPlayUpdatePing request) + { + var currentSession = GetSession(_sessionContext); + var syncPlayRequest = new PlaybackRequest() + { + Type = PlaybackRequestType.UpdatePing, + Ping = Convert.ToInt64(request.Ping) + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + } + } +} diff --git a/MediaBrowser.Api/SyncPlay/TimeSyncService.cs b/MediaBrowser.Api/SyncPlay/TimeSyncService.cs new file mode 100644 index 000000000..4a9307e62 --- /dev/null +++ b/MediaBrowser.Api/SyncPlay/TimeSyncService.cs @@ -0,0 +1,52 @@ +using System; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Services; +using MediaBrowser.Model.SyncPlay; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Api.SyncPlay +{ + [Route("/GetUtcTime", "GET", Summary = "Get UtcTime")] + public class GetUtcTime : IReturnVoid + { + // Nothing + } + + /// + /// Class TimeSyncService. + /// + public class TimeSyncService : BaseApiService + { + public TimeSyncService( + ILogger logger, + IServerConfigurationManager serverConfigurationManager, + IHttpResultFactory httpResultFactory) + : base(logger, serverConfigurationManager, httpResultFactory) + { + // Do nothing + } + + /// + /// Handles the specified request. + /// + /// The request. + /// The current UTC time response. + public UtcTimeResponse Get(GetUtcTime request) + { + // Important to keep the following line at the beginning + var requestReceptionTime = DateTime.UtcNow.ToUniversalTime().ToString("o"); + + var response = new UtcTimeResponse(); + response.RequestReceptionTime = requestReceptionTime; + + // Important to keep the following two lines at the end + var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime().ToString("o"); + response.ResponseTransmissionTime = responseTransmissionTime; + + // Implementing NTP on such a high level results in this useless + // information being sent. On the other hand it enables future additions. + return response; + } + } +} diff --git a/MediaBrowser.Api/Syncplay/SyncplayService.cs b/MediaBrowser.Api/Syncplay/SyncplayService.cs deleted file mode 100644 index 4b6e16762..000000000 --- a/MediaBrowser.Api/Syncplay/SyncplayService.cs +++ /dev/null @@ -1,312 +0,0 @@ -using System.Threading; -using System; -using System.Collections.Generic; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Session; -using MediaBrowser.Controller.Syncplay; -using MediaBrowser.Model.Services; -using MediaBrowser.Model.Syncplay; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.Syncplay -{ - [Route("/Syncplay/{SessionId}/NewGroup", "POST", Summary = "Create a new Syncplay group")] - [Authenticated] - public class SyncplayNewGroup : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - } - - [Route("/Syncplay/{SessionId}/JoinGroup", "POST", Summary = "Join an existing Syncplay group")] - [Authenticated] - public class SyncplayJoinGroup : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - - /// - /// Gets or sets the Group id. - /// - /// The Group id to join. - [ApiMember(Name = "GroupId", Description = "Group Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string GroupId { get; set; } - - /// - /// Gets or sets the playing item id. - /// - /// The client's currently playing item id. - [ApiMember(Name = "PlayingItemId", Description = "Client's playing item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string PlayingItemId { get; set; } - } - - [Route("/Syncplay/{SessionId}/LeaveGroup", "POST", Summary = "Leave joined Syncplay group")] - [Authenticated] - public class SyncplayLeaveGroup : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - } - - [Route("/Syncplay/{SessionId}/ListGroups", "POST", Summary = "List Syncplay groups")] - [Authenticated] - public class SyncplayListGroups : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - - /// - /// Gets or sets the filter item id. - /// - /// The filter item id. - [ApiMember(Name = "FilterItemId", Description = "Filter by item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string FilterItemId { get; set; } - } - - [Route("/Syncplay/{SessionId}/PlayRequest", "POST", Summary = "Request play in Syncplay group")] - [Authenticated] - public class SyncplayPlayRequest : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - } - - [Route("/Syncplay/{SessionId}/PauseRequest", "POST", Summary = "Request pause in Syncplay group")] - [Authenticated] - public class SyncplayPauseRequest : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - } - - [Route("/Syncplay/{SessionId}/SeekRequest", "POST", Summary = "Request seek in Syncplay group")] - [Authenticated] - public class SyncplaySeekRequest : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - - [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")] - public long PositionTicks { get; set; } - } - - [Route("/Syncplay/{SessionId}/BufferingRequest", "POST", Summary = "Request group wait in Syncplay group while buffering")] - [Authenticated] - public class SyncplayBufferingRequest : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - - /// - /// Gets or sets the date used to pin PositionTicks in time. - /// - /// The date related to PositionTicks. - [ApiMember(Name = "When", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string When { get; set; } - - [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")] - public long PositionTicks { get; set; } - - /// - /// Gets or sets whether this is a buffering or a buffering-done request. - /// - /// true if buffering is complete; false otherwise. - [ApiMember(Name = "BufferingDone", IsRequired = true, DataType = "bool", ParameterType = "query", Verb = "POST")] - public bool BufferingDone { get; set; } - } - - [Route("/Syncplay/{SessionId}/UpdatePing", "POST", Summary = "Update session ping")] - [Authenticated] - public class SyncplayUpdatePing : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - - [ApiMember(Name = "Ping", IsRequired = true, DataType = "double", ParameterType = "query", Verb = "POST")] - public double Ping { get; set; } - } - - /// - /// Class SyncplayService. - /// - public class SyncplayService : BaseApiService - { - /// - /// The session context. - /// - private readonly ISessionContext _sessionContext; - - /// - /// The Syncplay manager. - /// - private readonly ISyncplayManager _syncplayManager; - - public SyncplayService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - ISessionContext sessionContext, - ISyncplayManager syncplayManager) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _sessionContext = sessionContext; - _syncplayManager = syncplayManager; - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncplayNewGroup request) - { - var currentSession = GetSession(_sessionContext); - _syncplayManager.NewGroup(currentSession, CancellationToken.None); - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncplayJoinGroup request) - { - var currentSession = GetSession(_sessionContext); - var joinRequest = new JoinGroupRequest() - { - GroupId = Guid.Parse(request.GroupId) - }; - - // Both null and empty strings mean that client isn't playing anything - if (!String.IsNullOrEmpty(request.PlayingItemId)) - { - try - { - joinRequest.PlayingItemId = Guid.Parse(request.PlayingItemId); - } - catch (ArgumentNullException) - { - // Should never happen, but just in case - Logger.LogError("JoinGroup: null value for PlayingItemId. Ignoring request."); - return; - } - catch (FormatException) - { - Logger.LogError("JoinGroup: {0} is not a valid format for PlayingItemId. Ignoring request.", request.PlayingItemId); - return; - } - } - _syncplayManager.JoinGroup(currentSession, request.GroupId, joinRequest, CancellationToken.None); - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncplayLeaveGroup request) - { - var currentSession = GetSession(_sessionContext); - _syncplayManager.LeaveGroup(currentSession, CancellationToken.None); - } - - /// - /// Handles the specified request. - /// - /// The request. - /// The requested list of groups. - public List Post(SyncplayListGroups request) - { - var currentSession = GetSession(_sessionContext); - var filterItemId = Guid.Empty; - if (!String.IsNullOrEmpty(request.FilterItemId)) - { - try - { - filterItemId = Guid.Parse(request.FilterItemId); - } - catch (ArgumentNullException) - { - Logger.LogWarning("ListGroups: null value for FilterItemId. Ignoring filter."); - } - catch (FormatException) - { - Logger.LogWarning("ListGroups: {0} is not a valid format for FilterItemId. Ignoring filter.", request.FilterItemId); - } - } - return _syncplayManager.ListGroups(currentSession, filterItemId); - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncplayPlayRequest request) - { - var currentSession = GetSession(_sessionContext); - var syncplayRequest = new PlaybackRequest() - { - Type = PlaybackRequestType.Play - }; - _syncplayManager.HandleRequest(currentSession, syncplayRequest, CancellationToken.None); - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncplayPauseRequest request) - { - var currentSession = GetSession(_sessionContext); - var syncplayRequest = new PlaybackRequest() - { - Type = PlaybackRequestType.Pause - }; - _syncplayManager.HandleRequest(currentSession, syncplayRequest, CancellationToken.None); - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncplaySeekRequest request) - { - var currentSession = GetSession(_sessionContext); - var syncplayRequest = new PlaybackRequest() - { - Type = PlaybackRequestType.Seek, - PositionTicks = request.PositionTicks - }; - _syncplayManager.HandleRequest(currentSession, syncplayRequest, CancellationToken.None); - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncplayBufferingRequest request) - { - var currentSession = GetSession(_sessionContext); - var syncplayRequest = new PlaybackRequest() - { - Type = request.BufferingDone ? PlaybackRequestType.BufferingDone : PlaybackRequestType.Buffering, - When = DateTime.Parse(request.When), - PositionTicks = request.PositionTicks - }; - _syncplayManager.HandleRequest(currentSession, syncplayRequest, CancellationToken.None); - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncplayUpdatePing request) - { - var currentSession = GetSession(_sessionContext); - var syncplayRequest = new PlaybackRequest() - { - Type = PlaybackRequestType.UpdatePing, - Ping = Convert.ToInt64(request.Ping) - }; - _syncplayManager.HandleRequest(currentSession, syncplayRequest, CancellationToken.None); - } - } -} diff --git a/MediaBrowser.Api/Syncplay/TimeSyncService.cs b/MediaBrowser.Api/Syncplay/TimeSyncService.cs deleted file mode 100644 index 9a26ffd99..000000000 --- a/MediaBrowser.Api/Syncplay/TimeSyncService.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Services; -using MediaBrowser.Model.Syncplay; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.Syncplay -{ - [Route("/GetUtcTime", "GET", Summary = "Get UtcTime")] - public class GetUtcTime : IReturnVoid - { - // Nothing - } - - /// - /// Class TimeSyncService. - /// - public class TimeSyncService : BaseApiService - { - public TimeSyncService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory) - : base(logger, serverConfigurationManager, httpResultFactory) - { - // Do nothing - } - - /// - /// Handles the specified request. - /// - /// The request. - /// The current UTC time response. - public UtcTimeResponse Get(GetUtcTime request) - { - // Important to keep the following line at the beginning - var requestReceptionTime = DateTime.UtcNow.ToUniversalTime().ToString("o"); - - var response = new UtcTimeResponse(); - response.RequestReceptionTime = requestReceptionTime; - - // Important to keep the following two lines at the end - var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime().ToString("o"); - response.ResponseTransmissionTime = responseTransmissionTime; - - // Implementing NTP on such a high level results in this useless - // information being sent. On the other hand it enables future additions. - return response; - } - } -} diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 39c065b89..4c2f834cb 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -9,7 +9,7 @@ using MediaBrowser.Controller.Security; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Events; using MediaBrowser.Model.Session; -using MediaBrowser.Model.Syncplay; +using MediaBrowser.Model.SyncPlay; namespace MediaBrowser.Controller.Session { @@ -142,22 +142,22 @@ namespace MediaBrowser.Controller.Session Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken); /// - /// Sends the SyncplayCommand. + /// Sends the SyncPlayCommand. /// /// The session id. /// The command. /// The cancellation token. /// Task. - Task SendSyncplayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken); + Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken); /// - /// Sends the SyncplayGroupUpdate. + /// Sends the SyncPlayGroupUpdate. /// /// The session id. /// The group update. /// The cancellation token. /// Task. - Task SendSyncplayGroupUpdate(string sessionId, GroupUpdate command, CancellationToken cancellationToken); + Task SendSyncPlayGroupUpdate(string sessionId, GroupUpdate command, CancellationToken cancellationToken); /// /// Sends the browse command. diff --git a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs new file mode 100644 index 000000000..087748de0 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// + /// Class GroupInfo. + /// + /// + /// Class is not thread-safe, external locking is required when accessing methods. + /// + public class GroupInfo + { + /// + /// Default ping value used for sessions. + /// + public readonly long DefaulPing = 500; + /// + /// Gets or sets the group identifier. + /// + /// The group identifier. + public readonly Guid GroupId = Guid.NewGuid(); + + /// + /// Gets or sets the playing item. + /// + /// The playing item. + public BaseItem PlayingItem { get; set; } + + /// + /// Gets or sets whether playback is paused. + /// + /// Playback is paused. + public bool IsPaused { get; set; } + + /// + /// Gets or sets the position ticks. + /// + /// The position ticks. + public long PositionTicks { get; set; } + + /// + /// Gets or sets the last activity. + /// + /// The last activity. + public DateTime LastActivity { get; set; } + + /// + /// Gets the participants. + /// + /// The participants, or members of the group. + public readonly Dictionary Participants = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Checks if a session is in this group. + /// + /// true if the session is in this group; false otherwise. + public bool ContainsSession(string sessionId) + { + return Participants.ContainsKey(sessionId); + } + + /// + /// Adds the session to the group. + /// + /// The session. + public void AddSession(SessionInfo session) + { + if (ContainsSession(session.Id.ToString())) return; + var member = new GroupMember(); + member.Session = session; + member.Ping = DefaulPing; + member.IsBuffering = false; + Participants[session.Id.ToString()] = member; + } + + /// + /// Removes the session from the group. + /// + /// The session. + + public void RemoveSession(SessionInfo session) + { + if (!ContainsSession(session.Id.ToString())) return; + GroupMember member; + Participants.Remove(session.Id.ToString(), out member); + } + + /// + /// Updates the ping of a session. + /// + /// The session. + /// The ping. + public void UpdatePing(SessionInfo session, long ping) + { + if (!ContainsSession(session.Id.ToString())) return; + Participants[session.Id.ToString()].Ping = ping; + } + + /// + /// Gets the highest ping in the group. + /// + /// The highest ping in the group. + public long GetHighestPing() + { + long max = Int64.MinValue; + foreach (var session in Participants.Values) + { + max = Math.Max(max, session.Ping); + } + return max; + } + + /// + /// Sets the session's buffering state. + /// + /// The session. + /// The state. + public void SetBuffering(SessionInfo session, bool isBuffering) + { + if (!ContainsSession(session.Id.ToString())) return; + Participants[session.Id.ToString()].IsBuffering = isBuffering; + } + + /// + /// Gets the group buffering state. + /// + /// true if there is a session buffering in the group; false otherwise. + public bool IsBuffering() + { + foreach (var session in Participants.Values) + { + if (session.IsBuffering) return true; + } + return false; + } + + /// + /// Checks if the group is empty. + /// + /// true if the group is empty; false otherwise. + public bool IsEmpty() + { + return Participants.Count == 0; + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/GroupMember.cs b/MediaBrowser.Controller/SyncPlay/GroupMember.cs new file mode 100644 index 000000000..a3975c334 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/GroupMember.cs @@ -0,0 +1,28 @@ +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// + /// Class GroupMember. + /// + public class GroupMember + { + /// + /// Gets or sets whether this member is buffering. + /// + /// true if member is buffering; false otherwise. + public bool IsBuffering { get; set; } + + /// + /// Gets or sets the session. + /// + /// The session. + public SessionInfo Session { get; set; } + + /// + /// Gets or sets the ping. + /// + /// The ping. + public long Ping { get; set; } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs new file mode 100644 index 000000000..de1fcd259 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// + /// Interface ISyncPlayController. + /// + public interface ISyncPlayController + { + /// + /// Gets the group id. + /// + /// The group id. + Guid GetGroupId(); + + /// + /// Gets the playing item id. + /// + /// The playing item id. + Guid GetPlayingItemId(); + + /// + /// Checks if the group is empty. + /// + /// If the group is empty. + bool IsGroupEmpty(); + + /// + /// Initializes the group with the session's info. + /// + /// The session. + /// The cancellation token. + void InitGroup(SessionInfo session, CancellationToken cancellationToken); + + /// + /// Adds the session to the group. + /// + /// The session. + /// The request. + /// The cancellation token. + void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken); + + /// + /// Removes the session from the group. + /// + /// The session. + /// The cancellation token. + void SessionLeave(SessionInfo session, CancellationToken cancellationToken); + + /// + /// Handles the requested action by the session. + /// + /// The session. + /// The requested action. + /// The cancellation token. + void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken); + + /// + /// Gets the info about the group for the clients. + /// + /// The group info for the clients. + GroupInfoView GetInfo(); + } +} \ No newline at end of file diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs new file mode 100644 index 000000000..6c962ec85 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// + /// Interface ISyncPlayManager. + /// + public interface ISyncPlayManager + { + /// + /// Creates a new group. + /// + /// The session that's creating the group. + /// The cancellation token. + void NewGroup(SessionInfo session, CancellationToken cancellationToken); + + /// + /// Adds the session to a group. + /// + /// The session. + /// The group id. + /// The request. + /// The cancellation token. + void JoinGroup(SessionInfo session, string groupId, JoinGroupRequest request, CancellationToken cancellationToken); + + /// + /// Removes the session from a group. + /// + /// The session. + /// The cancellation token. + void LeaveGroup(SessionInfo session, CancellationToken cancellationToken); + + /// + /// Gets list of available groups for a session. + /// + /// The session. + /// The item id to filter by. + /// The list of available groups. + List ListGroups(SessionInfo session, Guid filterItemId); + + /// + /// Handle a request by a session in a group. + /// + /// The session. + /// The request. + /// The cancellation token. + void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken); + + /// + /// Maps a session to a group. + /// + /// The session. + /// The group. + /// + void AddSessionToGroup(SessionInfo session, ISyncPlayController group); + + /// + /// Unmaps a session from a group. + /// + /// The session. + /// The group. + /// + void RemoveSessionFromGroup(SessionInfo session, ISyncPlayController group); + } +} diff --git a/MediaBrowser.Controller/Syncplay/GroupInfo.cs b/MediaBrowser.Controller/Syncplay/GroupInfo.cs deleted file mode 100644 index c01fead83..000000000 --- a/MediaBrowser.Controller/Syncplay/GroupInfo.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System; -using System.Collections.Generic; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Session; - -namespace MediaBrowser.Controller.Syncplay -{ - /// - /// Class GroupInfo. - /// - /// - /// Class is not thread-safe, external locking is required when accessing methods. - /// - public class GroupInfo - { - /// - /// Default ping value used for sessions. - /// - public readonly long DefaulPing = 500; - /// - /// Gets or sets the group identifier. - /// - /// The group identifier. - public readonly Guid GroupId = Guid.NewGuid(); - - /// - /// Gets or sets the playing item. - /// - /// The playing item. - public BaseItem PlayingItem { get; set; } - - /// - /// Gets or sets whether playback is paused. - /// - /// Playback is paused. - public bool IsPaused { get; set; } - - /// - /// Gets or sets the position ticks. - /// - /// The position ticks. - public long PositionTicks { get; set; } - - /// - /// Gets or sets the last activity. - /// - /// The last activity. - public DateTime LastActivity { get; set; } - - /// - /// Gets the participants. - /// - /// The participants, or members of the group. - public readonly Dictionary Participants = - new Dictionary(StringComparer.OrdinalIgnoreCase); - - /// - /// Checks if a session is in this group. - /// - /// true if the session is in this group; false otherwise. - public bool ContainsSession(string sessionId) - { - return Participants.ContainsKey(sessionId); - } - - /// - /// Adds the session to the group. - /// - /// The session. - public void AddSession(SessionInfo session) - { - if (ContainsSession(session.Id.ToString())) return; - var member = new GroupMember(); - member.Session = session; - member.Ping = DefaulPing; - member.IsBuffering = false; - Participants[session.Id.ToString()] = member; - } - - /// - /// Removes the session from the group. - /// - /// The session. - - public void RemoveSession(SessionInfo session) - { - if (!ContainsSession(session.Id.ToString())) return; - GroupMember member; - Participants.Remove(session.Id.ToString(), out member); - } - - /// - /// Updates the ping of a session. - /// - /// The session. - /// The ping. - public void UpdatePing(SessionInfo session, long ping) - { - if (!ContainsSession(session.Id.ToString())) return; - Participants[session.Id.ToString()].Ping = ping; - } - - /// - /// Gets the highest ping in the group. - /// - /// The highest ping in the group. - public long GetHighestPing() - { - long max = Int64.MinValue; - foreach (var session in Participants.Values) - { - max = Math.Max(max, session.Ping); - } - return max; - } - - /// - /// Sets the session's buffering state. - /// - /// The session. - /// The state. - public void SetBuffering(SessionInfo session, bool isBuffering) - { - if (!ContainsSession(session.Id.ToString())) return; - Participants[session.Id.ToString()].IsBuffering = isBuffering; - } - - /// - /// Gets the group buffering state. - /// - /// true if there is a session buffering in the group; false otherwise. - public bool IsBuffering() - { - foreach (var session in Participants.Values) - { - if (session.IsBuffering) return true; - } - return false; - } - - /// - /// Checks if the group is empty. - /// - /// true if the group is empty; false otherwise. - public bool IsEmpty() - { - return Participants.Count == 0; - } - } -} diff --git a/MediaBrowser.Controller/Syncplay/GroupMember.cs b/MediaBrowser.Controller/Syncplay/GroupMember.cs deleted file mode 100644 index 7630428d7..000000000 --- a/MediaBrowser.Controller/Syncplay/GroupMember.cs +++ /dev/null @@ -1,28 +0,0 @@ -using MediaBrowser.Controller.Session; - -namespace MediaBrowser.Controller.Syncplay -{ - /// - /// Class GroupMember. - /// - public class GroupMember - { - /// - /// Gets or sets whether this member is buffering. - /// - /// true if member is buffering; false otherwise. - public bool IsBuffering { get; set; } - - /// - /// Gets or sets the session. - /// - /// The session. - public SessionInfo Session { get; set; } - - /// - /// Gets or sets the ping. - /// - /// The ping. - public long Ping { get; set; } - } -} diff --git a/MediaBrowser.Controller/Syncplay/ISyncplayController.cs b/MediaBrowser.Controller/Syncplay/ISyncplayController.cs deleted file mode 100644 index 34eae4062..000000000 --- a/MediaBrowser.Controller/Syncplay/ISyncplayController.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Threading; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Syncplay; - -namespace MediaBrowser.Controller.Syncplay -{ - /// - /// Interface ISyncplayController. - /// - public interface ISyncplayController - { - /// - /// Gets the group id. - /// - /// The group id. - Guid GetGroupId(); - - /// - /// Gets the playing item id. - /// - /// The playing item id. - Guid GetPlayingItemId(); - - /// - /// Checks if the group is empty. - /// - /// If the group is empty. - bool IsGroupEmpty(); - - /// - /// Initializes the group with the session's info. - /// - /// The session. - /// The cancellation token. - void InitGroup(SessionInfo session, CancellationToken cancellationToken); - - /// - /// Adds the session to the group. - /// - /// The session. - /// The request. - /// The cancellation token. - void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken); - - /// - /// Removes the session from the group. - /// - /// The session. - /// The cancellation token. - void SessionLeave(SessionInfo session, CancellationToken cancellationToken); - - /// - /// Handles the requested action by the session. - /// - /// The session. - /// The requested action. - /// The cancellation token. - void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken); - - /// - /// Gets the info about the group for the clients. - /// - /// The group info for the clients. - GroupInfoView GetInfo(); - } -} \ No newline at end of file diff --git a/MediaBrowser.Controller/Syncplay/ISyncplayManager.cs b/MediaBrowser.Controller/Syncplay/ISyncplayManager.cs deleted file mode 100644 index fbc208d27..000000000 --- a/MediaBrowser.Controller/Syncplay/ISyncplayManager.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Syncplay; - -namespace MediaBrowser.Controller.Syncplay -{ - /// - /// Interface ISyncplayManager. - /// - public interface ISyncplayManager - { - /// - /// Creates a new group. - /// - /// The session that's creating the group. - /// The cancellation token. - void NewGroup(SessionInfo session, CancellationToken cancellationToken); - - /// - /// Adds the session to a group. - /// - /// The session. - /// The group id. - /// The request. - /// The cancellation token. - void JoinGroup(SessionInfo session, string groupId, JoinGroupRequest request, CancellationToken cancellationToken); - - /// - /// Removes the session from a group. - /// - /// The session. - /// The cancellation token. - void LeaveGroup(SessionInfo session, CancellationToken cancellationToken); - - /// - /// Gets list of available groups for a session. - /// - /// The session. - /// The item id to filter by. - /// The list of available groups. - List ListGroups(SessionInfo session, Guid filterItemId); - - /// - /// Handle a request by a session in a group. - /// - /// The session. - /// The request. - /// The cancellation token. - void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken); - - /// - /// Maps a session to a group. - /// - /// The session. - /// The group. - /// - void AddSessionToGroup(SessionInfo session, ISyncplayController group); - - /// - /// Unmaps a session from a group. - /// - /// The session. - /// The group. - /// - void RemoveSessionFromGroup(SessionInfo session, ISyncplayController group); - } -} diff --git a/MediaBrowser.Model/Configuration/SyncplayAccess.cs b/MediaBrowser.Model/Configuration/SyncplayAccess.cs index cddf68c42..d891a8167 100644 --- a/MediaBrowser.Model/Configuration/SyncplayAccess.cs +++ b/MediaBrowser.Model/Configuration/SyncplayAccess.cs @@ -1,9 +1,9 @@ namespace MediaBrowser.Model.Configuration { /// - /// Enum SyncplayAccess. + /// Enum SyncPlayAccess. /// - public enum SyncplayAccess + public enum SyncPlayAccess { /// /// User can create groups and join them. @@ -16,7 +16,7 @@ namespace MediaBrowser.Model.Configuration JoinGroups, /// - /// Syncplay is disabled for the user. + /// SyncPlay is disabled for the user. /// None } diff --git a/MediaBrowser.Model/SyncPlay/GroupInfoView.cs b/MediaBrowser.Model/SyncPlay/GroupInfoView.cs new file mode 100644 index 000000000..7b833506b --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/GroupInfoView.cs @@ -0,0 +1,38 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// + /// Class GroupInfoView. + /// + public class GroupInfoView + { + /// + /// Gets or sets the group identifier. + /// + /// The group identifier. + public string GroupId { get; set; } + + /// + /// Gets or sets the playing item id. + /// + /// The playing item id. + public string PlayingItemId { get; set; } + + /// + /// Gets or sets the playing item name. + /// + /// The playing item name. + public string PlayingItemName { get; set; } + + /// + /// Gets or sets the position ticks. + /// + /// The position ticks. + public long PositionTicks { get; set; } + + /// + /// Gets or sets the participants. + /// + /// The participants. + public string[] Participants { get; set; } + } +} diff --git a/MediaBrowser.Model/SyncPlay/GroupUpdate.cs b/MediaBrowser.Model/SyncPlay/GroupUpdate.cs new file mode 100644 index 000000000..895702f3d --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/GroupUpdate.cs @@ -0,0 +1,26 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// + /// Class GroupUpdate. + /// + public class GroupUpdate + { + /// + /// Gets or sets the group identifier. + /// + /// The group identifier. + public string GroupId { get; set; } + + /// + /// Gets or sets the update type. + /// + /// The update type. + public GroupUpdateType Type { get; set; } + + /// + /// Gets or sets the data. + /// + /// The data. + public T Data { get; set; } + } +} diff --git a/MediaBrowser.Model/SyncPlay/GroupUpdateType.cs b/MediaBrowser.Model/SyncPlay/GroupUpdateType.cs new file mode 100644 index 000000000..89d245787 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/GroupUpdateType.cs @@ -0,0 +1,53 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// + /// Enum GroupUpdateType. + /// + public enum GroupUpdateType + { + /// + /// The user-joined update. Tells members of a group about a new user. + /// + UserJoined, + /// + /// The user-left update. Tells members of a group that a user left. + /// + UserLeft, + /// + /// The group-joined update. Tells a user that the group has been joined. + /// + GroupJoined, + /// + /// The group-left update. Tells a user that the group has been left. + /// + GroupLeft, + /// + /// The group-wait update. Tells members of the group that a user is buffering. + /// + GroupWait, + /// + /// The prepare-session update. Tells a user to load some content. + /// + PrepareSession, + /// + /// The not-in-group error. Tells a user that they don't belong to a group. + /// + NotInGroup, + /// + /// The group-does-not-exist error. Sent when trying to join a non-existing group. + /// + GroupDoesNotExist, + /// + /// The create-group-denied error. Sent when a user tries to create a group without required permissions. + /// + CreateGroupDenied, + /// + /// The join-group-denied error. Sent when a user tries to join a group without required permissions. + /// + JoinGroupDenied, + /// + /// The library-access-denied error. Sent when a user tries to join a group without required access to the library. + /// + LibraryAccessDenied + } +} diff --git a/MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs b/MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs new file mode 100644 index 000000000..d67b6bd55 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs @@ -0,0 +1,22 @@ +using System; + +namespace MediaBrowser.Model.SyncPlay +{ + /// + /// Class JoinGroupRequest. + /// + public class JoinGroupRequest + { + /// + /// Gets or sets the Group id. + /// + /// The Group id to join. + public Guid GroupId { get; set; } + + /// + /// Gets or sets the playing item id. + /// + /// The client's currently playing item id. + public Guid PlayingItemId { get; set; } + } +} diff --git a/MediaBrowser.Model/SyncPlay/PlaybackRequest.cs b/MediaBrowser.Model/SyncPlay/PlaybackRequest.cs new file mode 100644 index 000000000..9de23194e --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/PlaybackRequest.cs @@ -0,0 +1,34 @@ +using System; + +namespace MediaBrowser.Model.SyncPlay +{ + /// + /// Class PlaybackRequest. + /// + public class PlaybackRequest + { + /// + /// Gets or sets the request type. + /// + /// The request type. + public PlaybackRequestType Type { get; set; } + + /// + /// Gets or sets when the request has been made by the client. + /// + /// The date of the request. + public DateTime? When { get; set; } + + /// + /// Gets or sets the position ticks. + /// + /// The position ticks. + public long? PositionTicks { get; set; } + + /// + /// Gets or sets the ping time. + /// + /// The ping time. + public long? Ping { get; set; } + } +} diff --git a/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs b/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs new file mode 100644 index 000000000..f1e175fde --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs @@ -0,0 +1,33 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// + /// Enum PlaybackRequestType + /// + public enum PlaybackRequestType + { + /// + /// A user is requesting a play command for the group. + /// + Play = 0, + /// + /// A user is requesting a pause command for the group. + /// + Pause = 1, + /// + /// A user is requesting a seek command for the group. + /// + Seek = 2, + /// + /// A user is signaling that playback is buffering. + /// + Buffering = 3, + /// + /// A user is signaling that playback resumed. + /// + BufferingDone = 4, + /// + /// A user is reporting its ping. + /// + UpdatePing = 5 + } +} diff --git a/MediaBrowser.Model/SyncPlay/SendCommand.cs b/MediaBrowser.Model/SyncPlay/SendCommand.cs new file mode 100644 index 000000000..0f06e381f --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/SendCommand.cs @@ -0,0 +1,38 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// + /// Class SendCommand. + /// + public class SendCommand + { + /// + /// Gets or sets the group identifier. + /// + /// The group identifier. + public string GroupId { get; set; } + + /// + /// Gets or sets the UTC time when to execute the command. + /// + /// The UTC time when to execute the command. + public string When { get; set; } + + /// + /// Gets or sets the position ticks. + /// + /// The position ticks. + public long? PositionTicks { get; set; } + + /// + /// Gets or sets the command. + /// + /// The command. + public SendCommandType Command { get; set; } + + /// + /// Gets or sets the UTC time when this command has been emitted. + /// + /// The UTC time when this command has been emitted. + public string EmittedAt { get; set; } + } +} diff --git a/MediaBrowser.Model/SyncPlay/SendCommandType.cs b/MediaBrowser.Model/SyncPlay/SendCommandType.cs new file mode 100644 index 000000000..113719871 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/SendCommandType.cs @@ -0,0 +1,21 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// + /// Enum SendCommandType. + /// + public enum SendCommandType + { + /// + /// The play command. Instructs users to start playback. + /// + Play = 0, + /// + /// The pause command. Instructs users to pause playback. + /// + Pause = 1, + /// + /// The seek command. Instructs users to seek to a specified time. + /// + Seek = 2 + } +} diff --git a/MediaBrowser.Model/SyncPlay/UtcTimeResponse.cs b/MediaBrowser.Model/SyncPlay/UtcTimeResponse.cs new file mode 100644 index 000000000..0a6036154 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/UtcTimeResponse.cs @@ -0,0 +1,20 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// + /// Class UtcTimeResponse. + /// + public class UtcTimeResponse + { + /// + /// Gets or sets the UTC time when request has been received. + /// + /// The UTC time when request has been received. + public string RequestReceptionTime { get; set; } + + /// + /// Gets or sets the UTC time when response has been sent. + /// + /// The UTC time when response has been sent. + public string ResponseTransmissionTime { get; set; } + } +} diff --git a/MediaBrowser.Model/Syncplay/GroupInfoView.cs b/MediaBrowser.Model/Syncplay/GroupInfoView.cs deleted file mode 100644 index 50ad70630..000000000 --- a/MediaBrowser.Model/Syncplay/GroupInfoView.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace MediaBrowser.Model.Syncplay -{ - /// - /// Class GroupInfoView. - /// - public class GroupInfoView - { - /// - /// Gets or sets the group identifier. - /// - /// The group identifier. - public string GroupId { get; set; } - - /// - /// Gets or sets the playing item id. - /// - /// The playing item id. - public string PlayingItemId { get; set; } - - /// - /// Gets or sets the playing item name. - /// - /// The playing item name. - public string PlayingItemName { get; set; } - - /// - /// Gets or sets the position ticks. - /// - /// The position ticks. - public long PositionTicks { get; set; } - - /// - /// Gets or sets the participants. - /// - /// The participants. - public string[] Participants { get; set; } - } -} diff --git a/MediaBrowser.Model/Syncplay/GroupUpdate.cs b/MediaBrowser.Model/Syncplay/GroupUpdate.cs deleted file mode 100644 index cc49e92a9..000000000 --- a/MediaBrowser.Model/Syncplay/GroupUpdate.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace MediaBrowser.Model.Syncplay -{ - /// - /// Class GroupUpdate. - /// - public class GroupUpdate - { - /// - /// Gets or sets the group identifier. - /// - /// The group identifier. - public string GroupId { get; set; } - - /// - /// Gets or sets the update type. - /// - /// The update type. - public GroupUpdateType Type { get; set; } - - /// - /// Gets or sets the data. - /// - /// The data. - public T Data { get; set; } - } -} diff --git a/MediaBrowser.Model/Syncplay/GroupUpdateType.cs b/MediaBrowser.Model/Syncplay/GroupUpdateType.cs deleted file mode 100644 index 9f40f9577..000000000 --- a/MediaBrowser.Model/Syncplay/GroupUpdateType.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace MediaBrowser.Model.Syncplay -{ - /// - /// Enum GroupUpdateType. - /// - public enum GroupUpdateType - { - /// - /// The user-joined update. Tells members of a group about a new user. - /// - UserJoined, - /// - /// The user-left update. Tells members of a group that a user left. - /// - UserLeft, - /// - /// The group-joined update. Tells a user that the group has been joined. - /// - GroupJoined, - /// - /// The group-left update. Tells a user that the group has been left. - /// - GroupLeft, - /// - /// The group-wait update. Tells members of the group that a user is buffering. - /// - GroupWait, - /// - /// The prepare-session update. Tells a user to load some content. - /// - PrepareSession, - /// - /// The not-in-group error. Tells a user that they don't belong to a group. - /// - NotInGroup, - /// - /// The group-does-not-exist error. Sent when trying to join a non-existing group. - /// - GroupDoesNotExist, - /// - /// The create-group-denied error. Sent when a user tries to create a group without required permissions. - /// - CreateGroupDenied, - /// - /// The join-group-denied error. Sent when a user tries to join a group without required permissions. - /// - JoinGroupDenied, - /// - /// The library-access-denied error. Sent when a user tries to join a group without required access to the library. - /// - LibraryAccessDenied - } -} diff --git a/MediaBrowser.Model/Syncplay/JoinGroupRequest.cs b/MediaBrowser.Model/Syncplay/JoinGroupRequest.cs deleted file mode 100644 index 8d8a2646a..000000000 --- a/MediaBrowser.Model/Syncplay/JoinGroupRequest.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace MediaBrowser.Model.Syncplay -{ - /// - /// Class JoinGroupRequest. - /// - public class JoinGroupRequest - { - /// - /// Gets or sets the Group id. - /// - /// The Group id to join. - public Guid GroupId { get; set; } - - /// - /// Gets or sets the playing item id. - /// - /// The client's currently playing item id. - public Guid PlayingItemId { get; set; } - } -} diff --git a/MediaBrowser.Model/Syncplay/PlaybackRequest.cs b/MediaBrowser.Model/Syncplay/PlaybackRequest.cs deleted file mode 100644 index ba97641f6..000000000 --- a/MediaBrowser.Model/Syncplay/PlaybackRequest.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; - -namespace MediaBrowser.Model.Syncplay -{ - /// - /// Class PlaybackRequest. - /// - public class PlaybackRequest - { - /// - /// Gets or sets the request type. - /// - /// The request type. - public PlaybackRequestType Type { get; set; } - - /// - /// Gets or sets when the request has been made by the client. - /// - /// The date of the request. - public DateTime? When { get; set; } - - /// - /// Gets or sets the position ticks. - /// - /// The position ticks. - public long? PositionTicks { get; set; } - - /// - /// Gets or sets the ping time. - /// - /// The ping time. - public long? Ping { get; set; } - } -} diff --git a/MediaBrowser.Model/Syncplay/PlaybackRequestType.cs b/MediaBrowser.Model/Syncplay/PlaybackRequestType.cs deleted file mode 100644 index b3d49d09e..000000000 --- a/MediaBrowser.Model/Syncplay/PlaybackRequestType.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace MediaBrowser.Model.Syncplay -{ - /// - /// Enum PlaybackRequestType - /// - public enum PlaybackRequestType - { - /// - /// A user is requesting a play command for the group. - /// - Play = 0, - /// - /// A user is requesting a pause command for the group. - /// - Pause = 1, - /// - /// A user is requesting a seek command for the group. - /// - Seek = 2, - /// - /// A user is signaling that playback is buffering. - /// - Buffering = 3, - /// - /// A user is signaling that playback resumed. - /// - BufferingDone = 4, - /// - /// A user is reporting its ping. - /// - UpdatePing = 5 - } -} diff --git a/MediaBrowser.Model/Syncplay/SendCommand.cs b/MediaBrowser.Model/Syncplay/SendCommand.cs deleted file mode 100644 index d9f391403..000000000 --- a/MediaBrowser.Model/Syncplay/SendCommand.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace MediaBrowser.Model.Syncplay -{ - /// - /// Class SendCommand. - /// - public class SendCommand - { - /// - /// Gets or sets the group identifier. - /// - /// The group identifier. - public string GroupId { get; set; } - - /// - /// Gets or sets the UTC time when to execute the command. - /// - /// The UTC time when to execute the command. - public string When { get; set; } - - /// - /// Gets or sets the position ticks. - /// - /// The position ticks. - public long? PositionTicks { get; set; } - - /// - /// Gets or sets the command. - /// - /// The command. - public SendCommandType Command { get; set; } - - /// - /// Gets or sets the UTC time when this command has been emitted. - /// - /// The UTC time when this command has been emitted. - public string EmittedAt { get; set; } - } -} diff --git a/MediaBrowser.Model/Syncplay/SendCommandType.cs b/MediaBrowser.Model/Syncplay/SendCommandType.cs deleted file mode 100644 index 02e4774d0..000000000 --- a/MediaBrowser.Model/Syncplay/SendCommandType.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace MediaBrowser.Model.Syncplay -{ - /// - /// Enum SendCommandType. - /// - public enum SendCommandType - { - /// - /// The play command. Instructs users to start playback. - /// - Play = 0, - /// - /// The pause command. Instructs users to pause playback. - /// - Pause = 1, - /// - /// The seek command. Instructs users to seek to a specified time. - /// - Seek = 2 - } -} diff --git a/MediaBrowser.Model/Syncplay/UtcTimeResponse.cs b/MediaBrowser.Model/Syncplay/UtcTimeResponse.cs deleted file mode 100644 index f7887dc33..000000000 --- a/MediaBrowser.Model/Syncplay/UtcTimeResponse.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace MediaBrowser.Model.Syncplay -{ - /// - /// Class UtcTimeResponse. - /// - public class UtcTimeResponse - { - /// - /// Gets or sets the UTC time when request has been received. - /// - /// The UTC time when request has been received. - public string RequestReceptionTime { get; set; } - - /// - /// Gets or sets the UTC time when response has been sent. - /// - /// The UTC time when response has been sent. - public string ResponseTransmissionTime { get; set; } - } -} diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index cf576c358..3e027e831 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -81,10 +81,10 @@ namespace MediaBrowser.Model.Users public string PasswordResetProviderId { get; set; } /// - /// Gets or sets a value indicating what Syncplay features the user can access. + /// Gets or sets a value indicating what SyncPlay features the user can access. /// - /// Access level to Syncplay features. - public SyncplayAccess SyncplayAccess { get; set; } + /// Access level to SyncPlay features. + public SyncPlayAccess SyncPlayAccess { get; set; } public UserPolicy() { @@ -131,7 +131,7 @@ namespace MediaBrowser.Model.Users EnableContentDownloading = true; EnablePublicSharing = true; EnableRemoteAccess = true; - SyncplayAccess = SyncplayAccess.CreateAndJoinGroups; + SyncPlayAccess = SyncPlayAccess.CreateAndJoinGroups; } } } -- cgit v1.2.3 From 43c22a58229892836df645031fb570f37994e19e Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sun, 10 May 2020 14:36:11 -0400 Subject: Add GetLoopbackHttpApiUrl() helper method to replace forceHttps functionality Also refactor to use return a Uri instead of a string and use UriBuilder under the hood --- Emby.Server.Implementations/ApplicationHost.cs | 30 +++++++++++----------- .../Browser/BrowserLauncher.cs | 2 +- .../LiveTv/EmbyTV/EmbyTV.cs | 2 +- .../TunerHosts/HdHomerun/HdHomerunUdpStream.cs | 2 +- .../LiveTv/TunerHosts/SharedHttpStream.cs | 2 +- MediaBrowser.Controller/IServerApplicationHost.cs | 25 ++++++++++++++---- 6 files changed, 39 insertions(+), 24 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 8f20a4921..2201c3cfc 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1229,28 +1229,28 @@ namespace Emby.Server.Implementations str.CopyTo(span.Slice(1)); span[^1] = ']'; - return GetLocalApiUrl(span); + return GetLocalApiUrl(span).ToString(); } - return GetLocalApiUrl(ipAddress.ToString()); + return GetLocalApiUrl(ipAddress.ToString()).ToString(); } /// - public string GetLocalApiUrl(ReadOnlySpan host) + public Uri GetLoopbackHttpApiUrl() { - var url = new StringBuilder(64); - url.Append(ListenWithHttps ? "https://" : "http://") - .Append(host) - .Append(':') - .Append(ListenWithHttps ? HttpsPort : HttpPort); - - string baseUrl = ServerConfigurationManager.Configuration.BaseUrl; - if (baseUrl.Length != 0) - { - url.Append(baseUrl); - } + return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort); + } - return url.ToString(); + /// + public Uri GetLocalApiUrl(ReadOnlySpan host, string scheme = null, int? port = null) + { + return new UriBuilder + { + Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp), + Host = host.ToString(), + Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort), + Path = ServerConfigurationManager.Configuration.BaseUrl + }.Uri; } public Task> GetLocalIpAddresses(CancellationToken cancellationToken) diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs index 96096e142..ccb733b3c 100644 --- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs +++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs @@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Browser { try { - string baseUrl = appHost.GetLocalApiUrl("localhost"); + Uri baseUrl = appHost.GetLocalApiUrl("localhost"); appHost.LaunchUrl(baseUrl + url); } catch (Exception ex) diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 900f12062..3efe1ee25 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -1059,7 +1059,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var stream = new MediaSourceInfo { - EncoderPath = _appHost.GetLocalApiUrl("127.0.0.1") + "/LiveTv/LiveRecordings/" + info.Id + "/stream", + EncoderPath = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveRecordings/" + info.Id + "/stream", EncoderProtocol = MediaProtocol.Http, Path = info.Path, Protocol = MediaProtocol.File, diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 03ee5bfb6..82b1f3cf1 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun //OpenedMediaSource.Path = tempFile; //OpenedMediaSource.ReadAtNativeFramerate = true; - MediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1") + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; + MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; MediaSource.Protocol = MediaProtocol.Http; //OpenedMediaSource.SupportsDirectPlay = false; //OpenedMediaSource.SupportsDirectStream = true; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index d63588bbd..083fcd029 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -106,7 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts //OpenedMediaSource.Path = tempFile; //OpenedMediaSource.ReadAtNativeFramerate = true; - MediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1") + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; + MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; MediaSource.Protocol = MediaProtocol.Http; //OpenedMediaSource.Path = TempFilePath; diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 8537e4180..0028bd689 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -65,26 +65,41 @@ namespace MediaBrowser.Controller /// /// Gets a local (LAN) URL that can be used to access the API. The hostname used is the first valid configured - /// IP address that can be found via . + /// IP address that can be found via . HTTPS will be preferred when available. /// /// A cancellation token that can be used to cancel the task. /// The server URL. Task GetLocalApiUrl(CancellationToken cancellationToken); /// - /// Gets a local (LAN) URL that can be used to access the API. + /// Gets a local (LAN) URL that can be used to access the API using the loop-back IP address (127.0.0.1) + /// over HTTP (not HTTPS). /// - /// The hostname to use in the URL. /// The API URL. - string GetLocalApiUrl(ReadOnlySpan hostname); + public Uri GetLoopbackHttpApiUrl(); /// - /// Gets a local (LAN) URL that can be used to access the API. + /// Gets a local (LAN) URL that can be used to access the API. HTTPS will be preferred when available. /// /// The IP address to use as the hostname in the URL. /// The API URL. string GetLocalApiUrl(IPAddress address); + /// + /// Gets a local (LAN) URL that can be used to access the API. + /// + /// The hostname to use in the URL. + /// + /// The scheme to use for the URL. If null, the scheme will be selected automatically, + /// preferring HTTPS, if available. + /// + /// + /// The port to use for the URL. If null, the port will be selected automatically, + /// preferring the HTTPS port, if available. + /// + /// The API URL. + Uri GetLocalApiUrl(ReadOnlySpan hostname, string scheme = null, int? port = null); + /// /// Open a URL in an external browser window. /// -- cgit v1.2.3 From 3abf870c1e321dbeb484e4e255000a27760d7bc9 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sun, 10 May 2020 18:07:56 -0400 Subject: Do not include a double slash in URLs when a base URL is not set --- Emby.Server.Implementations/ApplicationHost.cs | 15 ++++++++------- Emby.Server.Implementations/Browser/BrowserLauncher.cs | 10 +++++----- MediaBrowser.Controller/IServerApplicationHost.cs | 4 ++-- 3 files changed, 15 insertions(+), 14 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 10a7e879e..693b049cd 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1236,28 +1236,30 @@ namespace Emby.Server.Implementations str.CopyTo(span.Slice(1)); span[^1] = ']'; - return GetLocalApiUrl(span).ToString(); + return GetLocalApiUrl(span); } - return GetLocalApiUrl(ipAddress.ToString()).ToString(); + return GetLocalApiUrl(ipAddress.ToString()); } /// - public Uri GetLoopbackHttpApiUrl() + public string GetLoopbackHttpApiUrl() { return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort); } /// - public Uri GetLocalApiUrl(ReadOnlySpan host, string scheme = null, int? port = null) + public string GetLocalApiUrl(ReadOnlySpan host, string scheme = null, int? port = null) { + // NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does + // not. For consistency, always trim the trailing slash. return new UriBuilder { Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp), Host = host.ToString(), Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort), Path = ServerConfigurationManager.Configuration.BaseUrl - }.Uri; + }.ToString().TrimEnd('/'); } public Task> GetLocalIpAddresses(CancellationToken cancellationToken) @@ -1333,8 +1335,7 @@ namespace Emby.Server.Implementations return true; } - var apiUrl = GetLocalApiUrl(address); - apiUrl += "/system/ping"; + var apiUrl = GetLocalApiUrl(address) + "/system/ping"; if (_validAddressResults.TryGetValue(apiUrl, out var cachedResult)) { diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs index ccb733b3c..7f7c6a0be 100644 --- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs +++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs @@ -31,18 +31,18 @@ namespace Emby.Server.Implementations.Browser /// Opens the specified URL in an external browser window. Any exceptions will be logged, but ignored. /// /// The application host. - /// The URL. - private static void TryOpenUrl(IServerApplicationHost appHost, string url) + /// The URL to open, relative to the server base URL. + private static void TryOpenUrl(IServerApplicationHost appHost, string relativeUrl) { try { - Uri baseUrl = appHost.GetLocalApiUrl("localhost"); - appHost.LaunchUrl(baseUrl + url); + string baseUrl = appHost.GetLocalApiUrl("localhost"); + appHost.LaunchUrl(baseUrl + relativeUrl); } catch (Exception ex) { var logger = appHost.Resolve(); - logger?.LogError(ex, "Failed to open browser window with URL {URL}", url); + logger?.LogError(ex, "Failed to open browser window with URL {URL}", relativeUrl); } } } diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 0028bd689..4f0ff1ee1 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -76,7 +76,7 @@ namespace MediaBrowser.Controller /// over HTTP (not HTTPS). /// /// The API URL. - public Uri GetLoopbackHttpApiUrl(); + string GetLoopbackHttpApiUrl(); /// /// Gets a local (LAN) URL that can be used to access the API. HTTPS will be preferred when available. @@ -98,7 +98,7 @@ namespace MediaBrowser.Controller /// preferring the HTTPS port, if available. /// /// The API URL. - Uri GetLocalApiUrl(ReadOnlySpan hostname, string scheme = null, int? port = null); + string GetLocalApiUrl(ReadOnlySpan hostname, string scheme = null, int? port = null); /// /// Open a URL in an external browser window. -- cgit v1.2.3 From 62420a6eb195f3119dc640b134d689d0de193a85 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 12 May 2020 16:03:15 -0400 Subject: Remove support for injecting ILogger directly --- Emby.Server.Implementations/ApplicationHost.cs | 7 ------- .../Library/Resolvers/Audio/MusicAlbumResolver.cs | 4 ++-- .../Library/Resolvers/Audio/MusicArtistResolver.cs | 6 +++--- .../LiveTv/TunerHosts/M3UTunerHost.cs | 4 ++-- MediaBrowser.Api/Movies/TrailersService.cs | 12 ++++++++--- MediaBrowser.Api/UserLibrary/ItemsService.cs | 2 +- .../Plugins/Omdb/OmdbEpisodeProvider.cs | 24 +++++++++++++--------- .../Plugins/Omdb/OmdbItemProvider.cs | 15 ++++++++------ 8 files changed, 40 insertions(+), 34 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index ffc916b98..b6e75b386 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -546,13 +546,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); - // TODO: Remove support for injecting ILogger completely - serviceCollection.AddSingleton((provider) => - { - Logger.LogWarning("Injecting ILogger directly is deprecated and should be replaced with ILogger"); - return Logger; - }); - serviceCollection.AddSingleton(_fileSystemManager); serviceCollection.AddSingleton(); diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index 85b1b6e32..6c9ba7c27 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// public class MusicAlbumResolver : ItemResolver { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IFileSystem _fileSystem; private readonly ILibraryManager _libraryManager; @@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// The logger. /// The file system. /// The library manager. - public MusicAlbumResolver(ILogger logger, IFileSystem fileSystem, ILibraryManager libraryManager) + public MusicAlbumResolver(ILogger logger, IFileSystem fileSystem, ILibraryManager libraryManager) { _logger = logger; _fileSystem = fileSystem; diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs index 681db4896..5f5cd0e92 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs @@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// public class MusicArtistResolver : ItemResolver { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IFileSystem _fileSystem; private readonly ILibraryManager _libraryManager; private readonly IServerConfigurationManager _config; @@ -23,12 +23,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// /// Initializes a new instance of the class. /// - /// The logger. + /// The logger for the created instances. /// The file system. /// The library manager. /// The configuration manager. public MusicArtistResolver( - ILogger logger, + ILogger logger, IFileSystem fileSystem, ILibraryManager libraryManager, IServerConfigurationManager config) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index f5dda79db..f7c9c736e 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts public M3UTunerHost( IServerConfigurationManager config, IMediaSourceManager mediaSourceManager, - ILogger logger, + ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IHttpClient httpClient, @@ -83,7 +83,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return Task.FromResult(list); } - private static readonly string[] _disallowedSharedStreamExtensions = new string[] + private static readonly string[] _disallowedSharedStreamExtensions = { ".mkv", ".mp4", diff --git a/MediaBrowser.Api/Movies/TrailersService.cs b/MediaBrowser.Api/Movies/TrailersService.cs index 8adf9c621..0b5334235 100644 --- a/MediaBrowser.Api/Movies/TrailersService.cs +++ b/MediaBrowser.Api/Movies/TrailersService.cs @@ -33,13 +33,18 @@ namespace MediaBrowser.Api.Movies /// private readonly ILibraryManager _libraryManager; + /// + /// The logger for the created instances. + /// + private readonly ILogger _logger; + private readonly IDtoService _dtoService; private readonly ILocalizationManager _localizationManager; private readonly IJsonSerializer _json; private readonly IAuthorizationContext _authContext; public TrailersService( - ILogger logger, + ILoggerFactory loggerFactory, IServerConfigurationManager serverConfigurationManager, IHttpResultFactory httpResultFactory, IUserManager userManager, @@ -48,7 +53,7 @@ namespace MediaBrowser.Api.Movies ILocalizationManager localizationManager, IJsonSerializer json, IAuthorizationContext authContext) - : base(logger, serverConfigurationManager, httpResultFactory) + : base(loggerFactory.CreateLogger(), serverConfigurationManager, httpResultFactory) { _userManager = userManager; _libraryManager = libraryManager; @@ -56,6 +61,7 @@ namespace MediaBrowser.Api.Movies _localizationManager = localizationManager; _json = json; _authContext = authContext; + _logger = loggerFactory.CreateLogger(); } public object Get(Getrailers request) @@ -66,7 +72,7 @@ namespace MediaBrowser.Api.Movies getItems.IncludeItemTypes = "Trailer"; return new ItemsService( - Logger, + _logger, ServerConfigurationManager, ResultFactory, _userManager, diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index c4d44042b..f3c0441e1 100644 --- a/MediaBrowser.Api/UserLibrary/ItemsService.cs +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -59,7 +59,7 @@ namespace MediaBrowser.Api.UserLibrary /// The localization. /// The dto service. public ItemsService( - ILogger logger, + ILogger logger, IServerConfigurationManager serverConfigurationManager, IHttpResultFactory httpResultFactory, IUserManager userManager, diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs index 37160dd2c..f0328e8d8 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs @@ -11,13 +11,10 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Plugins.Omdb { - public class OmdbEpisodeProvider : - IRemoteMetadataProvider, - IHasOrder + public class OmdbEpisodeProvider : IRemoteMetadataProvider, IHasOrder { private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClient _httpClient; @@ -26,16 +23,27 @@ namespace MediaBrowser.Providers.Plugins.Omdb private readonly IServerConfigurationManager _configurationManager; private readonly IApplicationHost _appHost; - public OmdbEpisodeProvider(IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClient httpClient, ILogger logger, ILibraryManager libraryManager, IFileSystem fileSystem, IServerConfigurationManager configurationManager) + public OmdbEpisodeProvider( + IJsonSerializer jsonSerializer, + IApplicationHost appHost, + IHttpClient httpClient, + ILibraryManager libraryManager, + IFileSystem fileSystem, + IServerConfigurationManager configurationManager) { _jsonSerializer = jsonSerializer; _httpClient = httpClient; _fileSystem = fileSystem; _configurationManager = configurationManager; _appHost = appHost; - _itemProvider = new OmdbItemProvider(jsonSerializer, _appHost, httpClient, logger, libraryManager, fileSystem, configurationManager); + _itemProvider = new OmdbItemProvider(jsonSerializer, _appHost, httpClient, libraryManager, fileSystem, configurationManager); } + // After TheTvDb + public int Order => 1; + + public string Name => "The Open Movie Database"; + public Task> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken) { return _itemProvider.GetSearchResults(searchInfo, "episode", cancellationToken); @@ -66,10 +74,6 @@ namespace MediaBrowser.Providers.Plugins.Omdb return result; } - // After TheTvDb - public int Order => 1; - - public string Name => "The Open Movie Database"; public Task GetImageResponse(string url, CancellationToken cancellationToken) { diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs index 3aadda5d0..64a75955a 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs @@ -17,7 +17,6 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Plugins.Omdb { @@ -26,22 +25,27 @@ namespace MediaBrowser.Providers.Plugins.Omdb { private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClient _httpClient; - private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly IFileSystem _fileSystem; private readonly IServerConfigurationManager _configurationManager; private readonly IApplicationHost _appHost; - public OmdbItemProvider(IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClient httpClient, ILogger logger, ILibraryManager libraryManager, IFileSystem fileSystem, IServerConfigurationManager configurationManager) + public OmdbItemProvider( + IJsonSerializer jsonSerializer, + IApplicationHost appHost, + IHttpClient httpClient, + ILibraryManager libraryManager, + IFileSystem fileSystem, + IServerConfigurationManager configurationManager) { _jsonSerializer = jsonSerializer; _httpClient = httpClient; - _logger = logger; _libraryManager = libraryManager; _fileSystem = fileSystem; _configurationManager = configurationManager; _appHost = appHost; } + // After primary option public int Order => 2; @@ -80,7 +84,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb var parsedName = _libraryManager.ParseName(name); var yearInName = parsedName.Year; name = parsedName.Name; - year = year ?? yearInName; + year ??= yearInName; } if (string.IsNullOrWhiteSpace(imdbId)) @@ -312,6 +316,5 @@ namespace MediaBrowser.Providers.Plugins.Omdb /// The results. public List Search { get; set; } } - } } -- cgit v1.2.3 From 9ad839c7766bd5d6121a10b2c306d6fef9666c52 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 12 May 2020 22:10:35 -0400 Subject: Initial migration code --- Emby.Dlna/ContentDirectory/ContentDirectory.cs | 13 +- Emby.Dlna/ContentDirectory/ControlHandler.cs | 89 +- Emby.Dlna/Didl/DidlBuilder.cs | 11 +- Emby.Dlna/PlayTo/PlayToController.cs | 8 +- Emby.Drawing/ImageProcessor.cs | 9 + Emby.Notifications/Api/NotificationsService.cs | 6 +- Emby.Notifications/NotificationManager.cs | 13 +- .../Activity/ActivityLogEntryPoint.cs | 42 +- Emby.Server.Implementations/ApplicationHost.cs | 15 +- .../Channels/ChannelManager.cs | 7 +- .../Collections/CollectionManager.cs | 4 +- .../Data/SqliteDisplayPreferencesRepository.cs | 225 ---- .../Data/SqliteItemRepository.cs | 4 +- .../Data/SqliteUserDataRepository.cs | 379 ------- .../Data/SqliteUserRepository.cs | 240 ----- .../Devices/DeviceManager.cs | 26 +- Emby.Server.Implementations/Dto/DtoService.cs | 23 +- .../EntryPoints/LibraryChangedNotifier.cs | 2 +- .../EntryPoints/RecordingNotifier.cs | 3 +- .../EntryPoints/RefreshUsersMetadata.cs | 77 -- .../EntryPoints/ServerEventNotifier.cs | 29 +- .../HttpServer/Security/AuthService.cs | 24 +- .../HttpServer/Security/AuthorizationContext.cs | 4 +- .../HttpServer/Security/SessionContext.cs | 4 +- .../Library/DefaultAuthenticationProvider.cs | 177 ---- .../Library/DefaultPasswordResetProvider.cs | 140 --- .../Library/InvalidAuthProvider.cs | 54 - .../Library/LibraryManager.cs | 22 +- .../Library/MediaSourceManager.cs | 52 +- .../Library/MediaStreamSelector.cs | 1 + .../Library/MusicManager.cs | 32 +- .../Library/SearchEngine.cs | 4 +- .../Library/UserDataManager.cs | 10 +- Emby.Server.Implementations/Library/UserManager.cs | 1107 -------------------- .../Library/UserViewManager.cs | 29 +- .../LiveTv/LiveTvManager.cs | 40 +- .../Playlists/ManualPlaylistsFolder.cs | 6 +- .../Playlists/PlaylistManager.cs | 4 +- .../Session/SessionManager.cs | 91 +- .../Sorting/DateLastMediaAddedComparer.cs | 2 +- .../Sorting/DatePlayedComparer.cs | 2 +- .../Sorting/IsFavoriteOrLikeComparer.cs | 2 +- .../Sorting/IsPlayedComparer.cs | 2 +- .../Sorting/IsUnplayedComparer.cs | 2 +- .../Sorting/PlayCountComparer.cs | 2 +- Emby.Server.Implementations/TV/TVSeriesManager.cs | 13 +- Jellyfin.Api/Auth/CustomAuthenticationHandler.cs | 5 +- Jellyfin.Api/Controllers/StartupController.cs | 4 +- Jellyfin.Data/DayOfWeekHelper.cs | 70 ++ Jellyfin.Data/Entities/AccessSchedule.cs | 68 ++ Jellyfin.Data/Entities/ImageInfo.cs | 25 + Jellyfin.Data/Entities/User.cs | 284 +++-- Jellyfin.Data/Enums/DynamicDayOfWeek.cs | 18 + Jellyfin.Data/Enums/PermissionKind.cs | 11 +- Jellyfin.Data/Enums/PreferenceKind.cs | 13 +- Jellyfin.Data/Enums/SubtitlePlaybackMode.cs | 13 + Jellyfin.Data/Enums/UnratedItem.cs | 17 + Jellyfin.Server.Implementations/JellyfinDb.cs | 2 +- .../User/DefaultAuthenticationProvider.cs | 185 ++++ .../User/DefaultPasswordResetProvider.cs | 126 +++ .../User/DeviceAccessEntryPoint.cs | 65 ++ .../User/InvalidAuthProvider.cs | 47 + .../User/UserManager.cs | 749 +++++++++++++ .../Migrations/Routines/MigrateUserDb.cs | 8 + MediaBrowser.Api/BaseApiService.cs | 5 +- MediaBrowser.Api/FilterService.cs | 2 +- MediaBrowser.Api/Images/ImageService.cs | 147 ++- MediaBrowser.Api/Library/LibraryService.cs | 8 +- MediaBrowser.Api/LiveTv/LiveTvService.cs | 3 +- MediaBrowser.Api/Movies/MoviesService.cs | 28 +- MediaBrowser.Api/Music/InstantMixService.cs | 2 +- MediaBrowser.Api/Playback/BaseStreamingService.cs | 3 +- MediaBrowser.Api/Playback/MediaInfoService.cs | 36 +- MediaBrowser.Api/Sessions/SessionService.cs | 5 +- MediaBrowser.Api/SuggestionsService.cs | 2 +- MediaBrowser.Api/TvShowsService.cs | 8 +- .../UserLibrary/BaseItemsByNameService.cs | 4 +- MediaBrowser.Api/UserLibrary/ItemsService.cs | 20 +- MediaBrowser.Api/UserLibrary/PlaystateService.cs | 2 +- MediaBrowser.Api/UserLibrary/UserLibraryService.cs | 2 +- MediaBrowser.Api/UserService.cs | 36 +- .../Authentication/IAuthenticationProvider.cs | 2 +- .../Authentication/IPasswordResetProvider.cs | 2 +- MediaBrowser.Controller/Channels/Channel.cs | 13 +- .../Collections/ICollectionManager.cs | 2 +- MediaBrowser.Controller/Devices/IDeviceManager.cs | 2 +- MediaBrowser.Controller/Drawing/IImageProcessor.cs | 3 + .../Drawing/ImageProcessorExtensions.cs | 1 + MediaBrowser.Controller/Dto/IDtoService.cs | 8 +- MediaBrowser.Controller/Entities/Audio/Audio.cs | 1 + .../Entities/Audio/MusicAlbum.cs | 8 +- .../Entities/Audio/MusicArtist.cs | 8 +- MediaBrowser.Controller/Entities/AudioBook.cs | 1 + MediaBrowser.Controller/Entities/BaseItem.cs | 69 +- MediaBrowser.Controller/Entities/Book.cs | 1 + .../Entities/DayOfWeekHelper.cs | 71 -- MediaBrowser.Controller/Entities/Folder.cs | 55 +- .../Entities/InternalItemsQuery.cs | 18 +- MediaBrowser.Controller/Entities/Movies/BoxSet.cs | 19 +- MediaBrowser.Controller/Entities/Movies/Movie.cs | 1 + MediaBrowser.Controller/Entities/MusicVideo.cs | 1 + MediaBrowser.Controller/Entities/TV/Episode.cs | 1 + MediaBrowser.Controller/Entities/TV/Season.cs | 17 +- MediaBrowser.Controller/Entities/TV/Series.cs | 43 +- MediaBrowser.Controller/Entities/Trailer.cs | 1 + MediaBrowser.Controller/Entities/User.cs | 262 ----- MediaBrowser.Controller/Entities/UserRootFolder.cs | 4 +- MediaBrowser.Controller/Entities/UserView.cs | 8 +- .../Entities/UserViewBuilder.cs | 86 +- MediaBrowser.Controller/Library/IIntroProvider.cs | 2 +- MediaBrowser.Controller/Library/ILibraryManager.cs | 12 +- .../Library/IMediaSourceManager.cs | 6 +- MediaBrowser.Controller/Library/IMusicManager.cs | 6 +- .../Library/IUserDataManager.cs | 8 +- MediaBrowser.Controller/Library/IUserManager.cs | 125 +-- .../Library/PlaybackProgressEventArgs.cs | 4 +- MediaBrowser.Controller/LiveTv/ILiveTvManager.cs | 12 +- MediaBrowser.Controller/LiveTv/LiveTvChannel.cs | 1 + MediaBrowser.Controller/LiveTv/LiveTvProgram.cs | 1 + .../MediaEncoding/EncodingHelper.cs | 7 +- .../MediaEncoding/EncodingJobInfo.cs | 17 +- MediaBrowser.Controller/Net/AuthorizationInfo.cs | 10 +- MediaBrowser.Controller/Net/IAuthService.cs | 2 +- MediaBrowser.Controller/Net/ISessionContext.cs | 2 +- .../Notifications/INotificationService.cs | 2 +- .../Notifications/UserNotification.cs | 2 +- .../Persistence/IUserRepository.cs | 27 - MediaBrowser.Controller/Playlists/Playlist.cs | 26 +- .../Providers/IProviderManager.cs | 3 + MediaBrowser.Controller/Session/ISessionManager.cs | 2 +- .../Sorting/IUserBaseItemComparer.cs | 2 +- MediaBrowser.Model/Configuration/AccessSchedule.cs | 2 + .../Configuration/DynamicDayOfWeek.cs | 18 - .../Configuration/SubtitlePlaybackMode.cs | 13 - MediaBrowser.Model/Configuration/UnratedItem.cs | 17 - .../Configuration/UserConfiguration.cs | 1 + .../Notifications/NotificationOptions.cs | 5 +- MediaBrowser.Model/Users/UserPolicy.cs | 7 +- MediaBrowser.Providers/Manager/ImageSaver.cs | 27 +- MediaBrowser.Providers/Manager/ProviderManager.cs | 14 +- .../Users/UserMetadataService.cs | 30 - .../Auth/CustomAuthenticationHandlerTests.cs | 13 +- 142 files changed, 2531 insertions(+), 3677 deletions(-) delete mode 100644 Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs delete mode 100644 Emby.Server.Implementations/Data/SqliteUserDataRepository.cs delete mode 100644 Emby.Server.Implementations/Data/SqliteUserRepository.cs delete mode 100644 Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs delete mode 100644 Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs delete mode 100644 Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs delete mode 100644 Emby.Server.Implementations/Library/InvalidAuthProvider.cs delete mode 100644 Emby.Server.Implementations/Library/UserManager.cs create mode 100644 Jellyfin.Data/DayOfWeekHelper.cs create mode 100644 Jellyfin.Data/Entities/AccessSchedule.cs create mode 100644 Jellyfin.Data/Entities/ImageInfo.cs create mode 100644 Jellyfin.Data/Enums/DynamicDayOfWeek.cs create mode 100644 Jellyfin.Data/Enums/SubtitlePlaybackMode.cs create mode 100644 Jellyfin.Data/Enums/UnratedItem.cs create mode 100644 Jellyfin.Server.Implementations/User/DefaultAuthenticationProvider.cs create mode 100644 Jellyfin.Server.Implementations/User/DefaultPasswordResetProvider.cs create mode 100644 Jellyfin.Server.Implementations/User/DeviceAccessEntryPoint.cs create mode 100644 Jellyfin.Server.Implementations/User/InvalidAuthProvider.cs create mode 100644 Jellyfin.Server.Implementations/User/UserManager.cs create mode 100644 Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs delete mode 100644 MediaBrowser.Controller/Entities/DayOfWeekHelper.cs delete mode 100644 MediaBrowser.Controller/Entities/User.cs delete mode 100644 MediaBrowser.Controller/Persistence/IUserRepository.cs delete mode 100644 MediaBrowser.Model/Configuration/DynamicDayOfWeek.cs delete mode 100644 MediaBrowser.Model/Configuration/SubtitlePlaybackMode.cs delete mode 100644 MediaBrowser.Model/Configuration/UnratedItem.cs delete mode 100644 MediaBrowser.Providers/Users/UserMetadataService.cs (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Dlna/ContentDirectory/ContentDirectory.cs b/Emby.Dlna/ContentDirectory/ContentDirectory.cs index 64cd308a2..ea577a905 100644 --- a/Emby.Dlna/ContentDirectory/ContentDirectory.cs +++ b/Emby.Dlna/ContentDirectory/ContentDirectory.cs @@ -1,8 +1,10 @@ #pragma warning disable CS1591 using System; +using System.Linq; using System.Threading.Tasks; using Emby.Dlna.Service; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dlna; @@ -104,7 +106,7 @@ namespace Emby.Dlna.ContentDirectory .ProcessControlRequestAsync(request); } - private User GetUser(DeviceProfile profile) + private Jellyfin.Data.Entities.User GetUser(DeviceProfile profile) { if (!string.IsNullOrEmpty(profile.UserId)) { @@ -130,18 +132,13 @@ namespace Emby.Dlna.ContentDirectory foreach (var user in _userManager.Users) { - if (user.Policy.IsAdministrator) + if (user.HasPermission(PermissionKind.IsAdministrator)) { return user; } } - foreach (var user in _userManager.Users) - { - return user; - } - - return null; + return _userManager.Users.FirstOrDefault(); } } } diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index 28888f031..32bda2fe1 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -36,7 +36,7 @@ namespace Emby.Dlna.ContentDirectory private readonly ILibraryManager _libraryManager; private readonly IUserDataManager _userDataManager; private readonly IServerConfigurationManager _config; - private readonly User _user; + private readonly Jellyfin.Data.Entities.User _user; private readonly IUserViewManager _userViewManager; private readonly ITVSeriesManager _tvSeriesManager; @@ -59,7 +59,7 @@ namespace Emby.Dlna.ContentDirectory string accessToken, IImageProcessor imageProcessor, IUserDataManager userDataManager, - User user, + Jellyfin.Data.Entities.User user, int systemUpdateId, IServerConfigurationManager config, ILocalizationManager localization, @@ -432,7 +432,7 @@ namespace Emby.Dlna.ContentDirectory xmlWriter.WriteElementString("UpdateID", _systemUpdateId.ToString(CultureInfo.InvariantCulture)); } - private QueryResult GetChildrenSorted(BaseItem item, User user, SearchCriteria search, SortCriteria sort, int? startIndex, int? limit) + private QueryResult GetChildrenSorted(BaseItem item, Jellyfin.Data.Entities.User user, SearchCriteria search, SortCriteria sort, int? startIndex, int? limit) { var folder = (Folder)item; @@ -489,7 +489,7 @@ namespace Emby.Dlna.ContentDirectory return new DtoOptions(true); } - private QueryResult GetUserItems(BaseItem item, StubType? stubType, User user, SortCriteria sort, int? startIndex, int? limit) + private QueryResult GetUserItems(BaseItem item, StubType? stubType, Jellyfin.Data.Entities.User user, SortCriteria sort, int? startIndex, int? limit) { if (item is MusicGenre) { @@ -558,7 +558,7 @@ namespace Emby.Dlna.ContentDirectory return ToResult(queryResult); } - private QueryResult GetLiveTvChannels(User user, SortCriteria sort, int? startIndex, int? limit) + private QueryResult GetLiveTvChannels(Jellyfin.Data.Entities.User user, SortCriteria sort, int? startIndex, int? limit) { var query = new InternalItemsQuery(user) { @@ -574,7 +574,7 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult GetMusicFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit) + private QueryResult GetMusicFolders(BaseItem item, Jellyfin.Data.Entities.User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit) { var query = new InternalItemsQuery(user) { @@ -692,7 +692,7 @@ namespace Emby.Dlna.ContentDirectory }; } - private QueryResult GetMovieFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit) + private QueryResult GetMovieFolders(BaseItem item, Jellyfin.Data.Entities.User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit) { var query = new InternalItemsQuery(user) { @@ -731,7 +731,7 @@ namespace Emby.Dlna.ContentDirectory return GetGenres(item, user, query); } - var array = new ServerItem[] + var array = new[] { new ServerItem(item) { @@ -766,7 +766,7 @@ namespace Emby.Dlna.ContentDirectory }; } - private QueryResult GetFolders(User user, int? startIndex, int? limit) + private QueryResult GetFolders(Jellyfin.Data.Entities.User user, int? startIndex, int? limit) { var folders = _libraryManager.GetUserRootFolder().GetChildren(user, true) .OrderBy(i => i.SortName) @@ -783,7 +783,7 @@ namespace Emby.Dlna.ContentDirectory }, startIndex, limit); } - private QueryResult GetTvFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit) + private QueryResult GetTvFolders(BaseItem item, Jellyfin.Data.Entities.User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit) { var query = new InternalItemsQuery(user) { @@ -871,7 +871,7 @@ namespace Emby.Dlna.ContentDirectory }; } - private QueryResult GetMovieContinueWatching(BaseItem parent, User user, InternalItemsQuery query) + private QueryResult GetMovieContinueWatching(BaseItem parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; @@ -891,7 +891,7 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult GetSeries(BaseItem parent, User user, InternalItemsQuery query) + private QueryResult GetSeries(BaseItem parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; @@ -904,7 +904,7 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult GetMovieMovies(BaseItem parent, User user, InternalItemsQuery query) + private QueryResult GetMovieMovies(BaseItem parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; @@ -917,7 +917,7 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult GetMovieCollections(User user, InternalItemsQuery query) + private QueryResult GetMovieCollections(Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.Recursive = true; //query.Parent = parent; @@ -930,7 +930,7 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult GetMusicAlbums(BaseItem parent, User user, InternalItemsQuery query) + private QueryResult GetMusicAlbums(BaseItem parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; @@ -943,7 +943,7 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult GetMusicSongs(BaseItem parent, User user, InternalItemsQuery query) + private QueryResult GetMusicSongs(BaseItem parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; @@ -956,7 +956,7 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult GetFavoriteSongs(BaseItem parent, User user, InternalItemsQuery query) + private QueryResult GetFavoriteSongs(BaseItem parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; @@ -969,7 +969,7 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult GetFavoriteSeries(BaseItem parent, User user, InternalItemsQuery query) + private QueryResult GetFavoriteSeries(BaseItem parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; @@ -982,7 +982,7 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult GetFavoriteEpisodes(BaseItem parent, User user, InternalItemsQuery query) + private QueryResult GetFavoriteEpisodes(BaseItem parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; @@ -995,7 +995,7 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult GetMovieFavorites(BaseItem parent, User user, InternalItemsQuery query) + private QueryResult GetMovieFavorites(BaseItem parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; @@ -1008,7 +1008,7 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult GetFavoriteAlbums(BaseItem parent, User user, InternalItemsQuery query) + private QueryResult GetFavoriteAlbums(BaseItem parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; @@ -1021,7 +1021,7 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult GetGenres(BaseItem parent, User user, InternalItemsQuery query) + private QueryResult GetGenres(BaseItem parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { var genresResult = _libraryManager.GetGenres(new InternalItemsQuery(user) { @@ -1039,7 +1039,7 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult GetMusicGenres(BaseItem parent, User user, InternalItemsQuery query) + private QueryResult GetMusicGenres(BaseItem parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { var genresResult = _libraryManager.GetMusicGenres(new InternalItemsQuery(user) { @@ -1057,7 +1057,7 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult GetMusicAlbumArtists(BaseItem parent, User user, InternalItemsQuery query) + private QueryResult GetMusicAlbumArtists(BaseItem parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { var artists = _libraryManager.GetAlbumArtists(new InternalItemsQuery(user) { @@ -1075,7 +1075,7 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult GetMusicArtists(BaseItem parent, User user, InternalItemsQuery query) + private QueryResult GetMusicArtists(BaseItem parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { var artists = _libraryManager.GetArtists(new InternalItemsQuery(user) { @@ -1093,7 +1093,7 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult GetFavoriteArtists(BaseItem parent, User user, InternalItemsQuery query) + private QueryResult GetFavoriteArtists(BaseItem parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { var artists = _libraryManager.GetArtists(new InternalItemsQuery(user) { @@ -1112,10 +1112,10 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult GetMusicPlaylists(User user, InternalItemsQuery query) + private QueryResult GetMusicPlaylists(Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.Parent = null; - query.IncludeItemTypes = new[] { typeof(Playlist).Name }; + query.IncludeItemTypes = new[] { nameof(Playlist) }; query.SetUser(user); query.Recursive = true; @@ -1124,7 +1124,7 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult GetMusicLatest(BaseItem parent, User user, InternalItemsQuery query) + private QueryResult GetMusicLatest(BaseItem parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.OrderBy = Array.Empty<(string, SortOrder)>(); @@ -1132,10 +1132,9 @@ namespace Emby.Dlna.ContentDirectory { UserId = user.Id, Limit = 50, - IncludeItemTypes = new[] { typeof(Audio).Name }, - ParentId = parent == null ? Guid.Empty : parent.Id, + IncludeItemTypes = new[] { nameof(Audio) }, + ParentId = parent?.Id ?? Guid.Empty, GroupItems = true - }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); return ToResult(items); @@ -1150,13 +1149,12 @@ namespace Emby.Dlna.ContentDirectory Limit = query.Limit, StartIndex = query.StartIndex, UserId = query.User.Id - }, new[] { parent }, query.DtoOptions); return ToResult(result); } - private QueryResult GetTvLatest(BaseItem parent, User user, InternalItemsQuery query) + private QueryResult GetTvLatest(BaseItem parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.OrderBy = Array.Empty<(string, SortOrder)>(); @@ -1167,30 +1165,29 @@ namespace Emby.Dlna.ContentDirectory IncludeItemTypes = new[] { typeof(Episode).Name }, ParentId = parent == null ? Guid.Empty : parent.Id, GroupItems = false - }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); return ToResult(items); } - private QueryResult GetMovieLatest(BaseItem parent, User user, InternalItemsQuery query) + private QueryResult GetMovieLatest(BaseItem parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.OrderBy = Array.Empty<(string, SortOrder)>(); - var items = _userViewManager.GetLatestItems(new LatestItemsQuery + var items = _userViewManager.GetLatestItems( + new LatestItemsQuery { UserId = user.Id, Limit = 50, - IncludeItemTypes = new[] { typeof(Movie).Name }, - ParentId = parent == null ? Guid.Empty : parent.Id, + IncludeItemTypes = new[] { nameof(Movie) }, + ParentId = parent?.Id ?? Guid.Empty, GroupItems = true - }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); return ToResult(items); } - private QueryResult GetMusicArtistItems(BaseItem item, Guid parentId, User user, SortCriteria sort, int? startIndex, int? limit) + private QueryResult GetMusicArtistItems(BaseItem item, Guid parentId, Jellyfin.Data.Entities.User user, SortCriteria sort, int? startIndex, int? limit) { var query = new InternalItemsQuery(user) { @@ -1210,14 +1207,18 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult GetGenreItems(BaseItem item, Guid parentId, User user, SortCriteria sort, int? startIndex, int? limit) + private QueryResult GetGenreItems(BaseItem item, Guid parentId, Jellyfin.Data.Entities.User user, SortCriteria sort, int? startIndex, int? limit) { var query = new InternalItemsQuery(user) { Recursive = true, ParentId = parentId, GenreIds = new[] { item.Id }, - IncludeItemTypes = new[] { typeof(Movie).Name, typeof(Series).Name }, + IncludeItemTypes = new[] + { + nameof(Movie), + nameof(Series) + }, Limit = limit, StartIndex = startIndex, DtoOptions = GetDtoOptions() @@ -1230,7 +1231,7 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult GetMusicGenreItems(BaseItem item, Guid parentId, User user, SortCriteria sort, int? startIndex, int? limit) + private QueryResult GetMusicGenreItems(BaseItem item, Guid parentId, Jellyfin.Data.Entities.User user, SortCriteria sort, int? startIndex, int? limit) { var query = new InternalItemsQuery(user) { diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index f7d840c62..24932ced9 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -6,7 +6,6 @@ using System.IO; using System.Linq; using System.Text; using System.Xml; -using Emby.Dlna.Configuration; using Emby.Dlna.ContentDirectory; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Drawing; @@ -39,7 +38,7 @@ namespace Emby.Dlna.Didl private readonly IImageProcessor _imageProcessor; private readonly string _serverAddress; private readonly string _accessToken; - private readonly User _user; + private readonly Jellyfin.Data.Entities.User _user; private readonly IUserDataManager _userDataManager; private readonly ILocalizationManager _localization; private readonly IMediaSourceManager _mediaSourceManager; @@ -49,7 +48,7 @@ namespace Emby.Dlna.Didl public DidlBuilder( DeviceProfile profile, - User user, + Jellyfin.Data.Entities.User user, IImageProcessor imageProcessor, string serverAddress, string accessToken, @@ -78,7 +77,7 @@ namespace Emby.Dlna.Didl return url + "&dlnaheaders=true"; } - public string GetItemDidl(BaseItem item, User user, BaseItem context, string deviceId, Filter filter, StreamInfo streamInfo) + public string GetItemDidl(BaseItem item, Jellyfin.Data.Entities.User user, BaseItem context, string deviceId, Filter filter, StreamInfo streamInfo) { var settings = new XmlWriterSettings { @@ -132,7 +131,7 @@ namespace Emby.Dlna.Didl public void WriteItemElement( XmlWriter writer, BaseItem item, - User user, + Jellyfin.Data.Entities.User user, BaseItem context, StubType? contextStubType, string deviceId, @@ -663,7 +662,7 @@ namespace Emby.Dlna.Didl writer.WriteFullEndElement(); } - private void AddSamsungBookmarkInfo(BaseItem item, User user, XmlWriter writer, StreamInfo streamInfo) + private void AddSamsungBookmarkInfo(BaseItem item, Jellyfin.Data.Entities.User user, XmlWriter writer, StreamInfo streamInfo) { if (!item.SupportsPositionTicksResume || item is Folder) { diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index 43e983054..ff37407a2 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -441,7 +441,13 @@ namespace Emby.Dlna.PlayTo } } - private PlaylistItem CreatePlaylistItem(BaseItem item, User user, long startPostionTicks, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex) + private PlaylistItem CreatePlaylistItem( + BaseItem item, + Jellyfin.Data.Entities.User user, + long startPostionTicks, + string mediaSourceId, + int? audioStreamIndex, + int? subtitleStreamIndex) { var deviceInfo = _device.Properties; diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs index 0b3bbe29e..b9172d2a8 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/Emby.Drawing/ImageProcessor.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; using MediaBrowser.Controller.Drawing; @@ -14,6 +15,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; +using Photo = MediaBrowser.Controller.Entities.Photo; namespace Emby.Drawing { @@ -328,6 +330,13 @@ namespace Emby.Drawing }); } + /// + public string GetImageCacheTag(User user) + { + return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5() + .ToString("N", CultureInfo.InvariantCulture); + } + private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified) { var inputFormat = Path.GetExtension(originalImagePath) diff --git a/Emby.Notifications/Api/NotificationsService.cs b/Emby.Notifications/Api/NotificationsService.cs index 788750796..221db5423 100644 --- a/Emby.Notifications/Api/NotificationsService.cs +++ b/Emby.Notifications/Api/NotificationsService.cs @@ -8,6 +8,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Notifications; @@ -164,7 +165,10 @@ namespace Emby.Notifications.Api Level = request.Level, Name = request.Name, Url = request.Url, - UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray() + UserIds = _userManager.Users + .Where(p => p.Permissions.Select(x => x.Kind).Contains(PermissionKind.IsAdministrator)) + .Select(p => p.Id) + .ToArray() }; return _notificationManager.SendNotification(notification, CancellationToken.None); diff --git a/Emby.Notifications/NotificationManager.cs b/Emby.Notifications/NotificationManager.cs index 639a5e1aa..9a9bc4415 100644 --- a/Emby.Notifications/NotificationManager.cs +++ b/Emby.Notifications/NotificationManager.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; @@ -81,7 +82,7 @@ namespace Emby.Notifications private Task SendNotification( NotificationRequest request, INotificationService service, - IEnumerable users, + IEnumerable users, string title, string description, CancellationToken cancellationToken) @@ -101,7 +102,7 @@ namespace Emby.Notifications switch (request.SendToUserMode.Value) { case SendToUserType.Admins: - return _userManager.Users.Where(i => i.Policy.IsAdministrator) + return _userManager.Users.Where(i => i.HasPermission(PermissionKind.IsAdministrator)) .Select(i => i.Id); case SendToUserType.All: return _userManager.UsersIds; @@ -117,7 +118,7 @@ namespace Emby.Notifications var config = GetConfiguration(); return _userManager.Users - .Where(i => config.IsEnabledToSendToUser(request.NotificationType, i.Id.ToString("N", CultureInfo.InvariantCulture), i.Policy)) + .Where(i => config.IsEnabledToSendToUser(request.NotificationType, i.Id.ToString("N", CultureInfo.InvariantCulture), i)) .Select(i => i.Id); } @@ -129,7 +130,7 @@ namespace Emby.Notifications INotificationService service, string title, string description, - User user, + Jellyfin.Data.Entities.User user, CancellationToken cancellationToken) { var notification = new UserNotification @@ -142,7 +143,7 @@ namespace Emby.Notifications User = user }; - _logger.LogDebug("Sending notification via {0} to user {1}", service.Name, user.Name); + _logger.LogDebug("Sending notification via {0} to user {1}", service.Name, user.Username); try { @@ -154,7 +155,7 @@ namespace Emby.Notifications } } - private bool IsEnabledForUser(INotificationService service, User user) + private bool IsEnabledForUser(INotificationService service, Jellyfin.Data.Entities.User user) { try { diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs index 54894fd65..3d3859078 100644 --- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs +++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs @@ -93,11 +93,10 @@ namespace Emby.Server.Implementations.Activity _subManager.SubtitleDownloadFailure += OnSubtitleDownloadFailure; - _userManager.UserCreated += OnUserCreated; - _userManager.UserPasswordChanged += OnUserPasswordChanged; - _userManager.UserDeleted += OnUserDeleted; - _userManager.UserPolicyUpdated += OnUserPolicyUpdated; - _userManager.UserLockedOut += OnUserLockedOut; + _userManager.OnUserCreated += OnUserCreated; + _userManager.OnUserPasswordChanged += OnUserPasswordChanged; + _userManager.OnUserDeleted += OnUserDeleted; + _userManager.OnUserLockedOut += OnUserLockedOut; _deviceManager.CameraImageUploaded += OnCameraImageUploaded; @@ -118,13 +117,13 @@ namespace Emby.Server.Implementations.Activity .ConfigureAwait(false); } - private async void OnUserLockedOut(object sender, GenericEventArgs e) + private async void OnUserLockedOut(object sender, GenericEventArgs e) { await CreateLogEntry(new ActivityLog( string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserLockedOutWithName"), - e.Argument.Name), + e.Argument.Username), NotificationType.UserLockedOut.ToString(), e.Argument.Id, DateTime.UtcNow, @@ -177,7 +176,7 @@ namespace Emby.Server.Implementations.Activity string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserStoppedPlayingItemWithValues"), - user.Name, + user.Username, GetItemName(item), e.DeviceName), GetPlaybackStoppedNotificationType(item.MediaType), @@ -214,7 +213,7 @@ namespace Emby.Server.Implementations.Activity string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserStartedPlayingItemWithValues"), - user.Name, + user.Username, GetItemName(item), e.DeviceName), GetPlaybackNotificationType(item.MediaType), @@ -338,13 +337,13 @@ namespace Emby.Server.Implementations.Activity }).ConfigureAwait(false); } - private async void OnUserPolicyUpdated(object sender, GenericEventArgs e) + private async void OnUserPolicyUpdated(object sender, GenericEventArgs e) { await CreateLogEntry(new ActivityLog( string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserPolicyUpdatedWithName"), - e.Argument.Name), + e.Argument.Username), "UserPolicyUpdated", e.Argument.Id, DateTime.UtcNow, @@ -352,13 +351,13 @@ namespace Emby.Server.Implementations.Activity .ConfigureAwait(false); } - private async void OnUserDeleted(object sender, GenericEventArgs e) + private async void OnUserDeleted(object sender, GenericEventArgs e) { await CreateLogEntry(new ActivityLog( string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDeletedWithName"), - e.Argument.Name), + e.Argument.Username), "UserDeleted", Guid.Empty, DateTime.UtcNow, @@ -366,26 +365,26 @@ namespace Emby.Server.Implementations.Activity .ConfigureAwait(false); } - private async void OnUserPasswordChanged(object sender, GenericEventArgs e) + private async void OnUserPasswordChanged(object sender, GenericEventArgs e) { await CreateLogEntry(new ActivityLog( string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserPasswordChangedWithName"), - e.Argument.Name), + e.Argument.Username), "UserPasswordChanged", e.Argument.Id, DateTime.UtcNow, LogLevel.Trace)).ConfigureAwait(false); } - private async void OnUserCreated(object sender, GenericEventArgs e) + private async void OnUserCreated(object sender, GenericEventArgs e) { await CreateLogEntry(new ActivityLog( string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserCreatedWithName"), - e.Argument.Name), + e.Argument.Username), "UserCreated", e.Argument.Id, DateTime.UtcNow, @@ -562,11 +561,10 @@ namespace Emby.Server.Implementations.Activity _subManager.SubtitleDownloadFailure -= OnSubtitleDownloadFailure; - _userManager.UserCreated -= OnUserCreated; - _userManager.UserPasswordChanged -= OnUserPasswordChanged; - _userManager.UserDeleted -= OnUserDeleted; - _userManager.UserPolicyUpdated -= OnUserPolicyUpdated; - _userManager.UserLockedOut -= OnUserLockedOut; + _userManager.OnUserCreated -= OnUserCreated; + _userManager.OnUserPasswordChanged -= OnUserPasswordChanged; + _userManager.OnUserDeleted -= OnUserDeleted; + _userManager.OnUserLockedOut -= OnUserLockedOut; _deviceManager.CameraImageUploaded -= OnCameraImageUploaded; } diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index ddd9c7953..52b48d081 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -48,6 +48,7 @@ using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Activity; +using Jellyfin.Server.Implementations.User; using MediaBrowser.Api; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; @@ -595,17 +596,12 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - // TODO: Refactor to eliminate the circular dependency here so that Lazy isn't required serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); serviceCollection.AddSingleton(); @@ -700,17 +696,11 @@ namespace Emby.Server.Implementations _httpServer = Resolve(); _httpClient = Resolve(); - ((SqliteDisplayPreferencesRepository)Resolve()).Initialize(); ((AuthenticationRepository)Resolve()).Initialize(); - ((SqliteUserRepository)Resolve()).Initialize(); SetStaticProperties(); - var userManager = (UserManager)Resolve(); - userManager.Initialize(); - - var userDataRepo = (SqliteUserDataRepository)Resolve(); - ((SqliteItemRepository)Resolve()).Initialize(userDataRepo, userManager); + ((SqliteItemRepository)Resolve()).Initialize(); FindParts(); } @@ -793,7 +783,6 @@ namespace Emby.Server.Implementations BaseItem.ProviderManager = Resolve(); BaseItem.LocalizationManager = Resolve(); BaseItem.ItemRepository = Resolve(); - User.UserManager = Resolve(); BaseItem.FileSystem = _fileSystemManager; BaseItem.UserDataManager = Resolve(); BaseItem.ChannelManager = Resolve(); diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 138832fb8..cb320dcb1 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -46,7 +46,7 @@ namespace Emby.Server.Implementations.Channels new ConcurrentDictionary>>(); private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1); - + /// /// Initializes a new instance of the class. /// @@ -791,8 +791,9 @@ namespace Emby.Server.Implementations.Channels return result; } - private async Task GetChannelItems(IChannel channel, - User user, + private async Task GetChannelItems( + IChannel channel, + Jellyfin.Data.Entities.User user, string externalFolderId, ChannelItemSortField? sortField, bool sortDescending, diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index 7c518d483..61963b633 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.Collections return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded); } - private IEnumerable GetCollections(User user) + private IEnumerable GetCollections(Jellyfin.Data.Entities.User user) { var folder = GetCollectionsFolder(false).Result; @@ -325,7 +325,7 @@ namespace Emby.Server.Implementations.Collections } /// - public IEnumerable CollapseItemsWithinBoxSets(IEnumerable items, User user) + public IEnumerable CollapseItemsWithinBoxSets(IEnumerable items, Jellyfin.Data.Entities.User user) { var results = new Dictionary(); diff --git a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs deleted file mode 100644 index d474f1c6b..000000000 --- a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs +++ /dev/null @@ -1,225 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Text.Json; -using System.Threading; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Json; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; - -namespace Emby.Server.Implementations.Data -{ - /// - /// Class SQLiteDisplayPreferencesRepository. - /// - public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository - { - private readonly IFileSystem _fileSystem; - - private readonly JsonSerializerOptions _jsonOptions; - - public SqliteDisplayPreferencesRepository(ILogger logger, IApplicationPaths appPaths, IFileSystem fileSystem) - : base(logger) - { - _fileSystem = fileSystem; - - _jsonOptions = JsonDefaults.GetOptions(); - - DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db"); - } - - /// - /// Gets the name of the repository. - /// - /// The name. - public string Name => "SQLite"; - - public void Initialize() - { - try - { - InitializeInternal(); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error loading database file. Will reset and retry."); - - _fileSystem.DeleteFile(DbFilePath); - - InitializeInternal(); - } - } - - /// - /// Opens the connection to the database - /// - /// Task. - private void InitializeInternal() - { - string[] queries = - { - "create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)", - "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)" - }; - - using (var connection = GetConnection()) - { - connection.RunQueries(queries); - } - } - - /// - /// Save the display preferences associated with an item in the repo - /// - /// The display preferences. - /// The user id. - /// The client. - /// The cancellation token. - /// item - public void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, CancellationToken cancellationToken) - { - if (displayPreferences == null) - { - throw new ArgumentNullException(nameof(displayPreferences)); - } - - if (string.IsNullOrEmpty(displayPreferences.Id)) - { - throw new ArgumentException("Display preferences has an invalid Id", nameof(displayPreferences)); - } - - cancellationToken.ThrowIfCancellationRequested(); - - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => SaveDisplayPreferences(displayPreferences, userId, client, db), - TransactionMode); - } - } - - private void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, IDatabaseConnection connection) - { - var serialized = JsonSerializer.SerializeToUtf8Bytes(displayPreferences, _jsonOptions); - - using (var statement = connection.PrepareStatement("replace into userdisplaypreferences (id, userid, client, data) values (@id, @userId, @client, @data)")) - { - statement.TryBind("@id", new Guid(displayPreferences.Id).ToByteArray()); - statement.TryBind("@userId", userId.ToByteArray()); - statement.TryBind("@client", client); - statement.TryBind("@data", serialized); - - statement.MoveNext(); - } - } - - /// - /// Save all display preferences associated with a user in the repo - /// - /// The display preferences. - /// The user id. - /// The cancellation token. - /// item - public void SaveAllDisplayPreferences(IEnumerable displayPreferences, Guid userId, CancellationToken cancellationToken) - { - if (displayPreferences == null) - { - throw new ArgumentNullException(nameof(displayPreferences)); - } - - cancellationToken.ThrowIfCancellationRequested(); - - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => - { - foreach (var displayPreference in displayPreferences) - { - SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db); - } - }, - TransactionMode); - } - } - - /// - /// Gets the display preferences. - /// - /// The display preferences id. - /// The user id. - /// The client. - /// Task{DisplayPreferences}. - /// item - public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, Guid userId, string client) - { - if (string.IsNullOrEmpty(displayPreferencesId)) - { - throw new ArgumentNullException(nameof(displayPreferencesId)); - } - - var guidId = displayPreferencesId.GetMD5(); - - using (var connection = GetConnection(true)) - { - using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where id = @id and userId=@userId and client=@client")) - { - statement.TryBind("@id", guidId.ToByteArray()); - statement.TryBind("@userId", userId.ToByteArray()); - statement.TryBind("@client", client); - - foreach (var row in statement.ExecuteQuery()) - { - return Get(row); - } - } - } - - return new DisplayPreferences - { - Id = guidId.ToString("N", CultureInfo.InvariantCulture) - }; - } - - /// - /// Gets all display preferences for the given user. - /// - /// The user id. - /// Task{DisplayPreferences}. - /// item - public IEnumerable GetAllDisplayPreferences(Guid userId) - { - var list = new List(); - - using (var connection = GetConnection(true)) - using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId")) - { - statement.TryBind("@userId", userId.ToByteArray()); - - foreach (var row in statement.ExecuteQuery()) - { - list.Add(Get(row)); - } - } - - return list; - } - - private DisplayPreferences Get(IReadOnlyList row) - => JsonSerializer.Deserialize(row[0].ToBlob(), _jsonOptions); - - public void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken) - => SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken); - - public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client) - => GetDisplayPreferences(displayPreferencesId, new Guid(userId), client); - } -} diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index ca5cd6fdd..74b8ffc18 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -102,7 +102,7 @@ namespace Emby.Server.Implementations.Data /// /// Opens the connection to the database /// - public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager) + public void Initialize() { const string CreateMediaStreamsTableCommand = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, PRIMARY KEY (ItemId, StreamIndex))"; @@ -324,8 +324,6 @@ namespace Emby.Server.Implementations.Data connection.RunQueries(postQueries); } - - userDataRepo.Initialize(userManager, WriteLock, WriteConnection); } private static readonly string[] _retriveItemColumns = diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs deleted file mode 100644 index 22955850a..000000000 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ /dev/null @@ -1,379 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Persistence; -using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; - -namespace Emby.Server.Implementations.Data -{ - public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository - { - public SqliteUserDataRepository( - ILogger logger, - IApplicationPaths appPaths) - : base(logger) - { - DbFilePath = Path.Combine(appPaths.DataPath, "library.db"); - } - - /// - public string Name => "SQLite"; - - /// - /// Opens the connection to the database. - /// - public void Initialize(IUserManager userManager, SemaphoreSlim dbLock, SQLiteDatabaseConnection dbConnection) - { - WriteLock.Dispose(); - WriteLock = dbLock; - WriteConnection?.Dispose(); - WriteConnection = dbConnection; - - using (var connection = GetConnection()) - { - var userDatasTableExists = TableExists(connection, "UserDatas"); - var userDataTableExists = TableExists(connection, "userdata"); - - var users = userDatasTableExists ? null : userManager.Users; - - connection.RunInTransaction(db => - { - db.ExecuteAll(string.Join(";", new[] { - - "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)", - - "drop index if exists idx_userdata", - "drop index if exists idx_userdata1", - "drop index if exists idx_userdata2", - "drop index if exists userdataindex1", - "drop index if exists userdataindex", - "drop index if exists userdataindex3", - "drop index if exists userdataindex4", - "create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)", - "create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)", - "create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)", - "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)" - })); - - if (userDataTableExists) - { - var existingColumnNames = GetColumnNames(db, "userdata"); - - AddColumn(db, "userdata", "InternalUserId", "int", existingColumnNames); - AddColumn(db, "userdata", "AudioStreamIndex", "int", existingColumnNames); - AddColumn(db, "userdata", "SubtitleStreamIndex", "int", existingColumnNames); - - if (!userDatasTableExists) - { - ImportUserIds(db, users); - - db.ExecuteAll("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null"); - } - } - }, TransactionMode); - } - } - - private void ImportUserIds(IDatabaseConnection db, IEnumerable users) - { - var userIdsWithUserData = GetAllUserIdsWithUserData(db); - - using (var statement = db.PrepareStatement("update userdata set InternalUserId=@InternalUserId where UserId=@UserId")) - { - foreach (var user in users) - { - if (!userIdsWithUserData.Contains(user.Id)) - { - continue; - } - - statement.TryBind("@UserId", user.Id.ToByteArray()); - statement.TryBind("@InternalUserId", user.InternalId); - - statement.MoveNext(); - statement.Reset(); - } - } - } - - private List GetAllUserIdsWithUserData(IDatabaseConnection db) - { - var list = new List(); - - using (var statement = PrepareStatement(db, "select DISTINCT UserId from UserData where UserId not null")) - { - foreach (var row in statement.ExecuteQuery()) - { - try - { - list.Add(row[0].ReadGuidFromBlob()); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error while getting user"); - } - } - } - - return list; - } - - /// - /// Saves the user data. - /// - public void SaveUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken) - { - if (userData == null) - { - throw new ArgumentNullException(nameof(userData)); - } - if (internalUserId <= 0) - { - throw new ArgumentNullException(nameof(internalUserId)); - } - if (string.IsNullOrEmpty(key)) - { - throw new ArgumentNullException(nameof(key)); - } - - PersistUserData(internalUserId, key, userData, cancellationToken); - } - - public void SaveAllUserData(long internalUserId, UserItemData[] userData, CancellationToken cancellationToken) - { - if (userData == null) - { - throw new ArgumentNullException(nameof(userData)); - } - if (internalUserId <= 0) - { - throw new ArgumentNullException(nameof(internalUserId)); - } - - PersistAllUserData(internalUserId, userData, cancellationToken); - } - - /// - /// Persists the user data. - /// - /// The user id. - /// The key. - /// The user data. - /// The cancellation token. - /// Task. - public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - using (var connection = GetConnection()) - { - connection.RunInTransaction(db => - { - SaveUserData(db, internalUserId, key, userData); - }, TransactionMode); - } - } - - private static void SaveUserData(IDatabaseConnection db, long internalUserId, string key, UserItemData userData) - { - using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)")) - { - statement.TryBind("@userId", internalUserId); - statement.TryBind("@key", key); - - if (userData.Rating.HasValue) - { - statement.TryBind("@rating", userData.Rating.Value); - } - else - { - statement.TryBindNull("@rating"); - } - - statement.TryBind("@played", userData.Played); - statement.TryBind("@playCount", userData.PlayCount); - statement.TryBind("@isFavorite", userData.IsFavorite); - statement.TryBind("@playbackPositionTicks", userData.PlaybackPositionTicks); - - if (userData.LastPlayedDate.HasValue) - { - statement.TryBind("@lastPlayedDate", userData.LastPlayedDate.Value.ToDateTimeParamValue()); - } - else - { - statement.TryBindNull("@lastPlayedDate"); - } - - if (userData.AudioStreamIndex.HasValue) - { - statement.TryBind("@AudioStreamIndex", userData.AudioStreamIndex.Value); - } - else - { - statement.TryBindNull("@AudioStreamIndex"); - } - - if (userData.SubtitleStreamIndex.HasValue) - { - statement.TryBind("@SubtitleStreamIndex", userData.SubtitleStreamIndex.Value); - } - else - { - statement.TryBindNull("@SubtitleStreamIndex"); - } - - statement.MoveNext(); - } - } - - /// - /// Persist all user data for the specified user - /// - private void PersistAllUserData(long internalUserId, UserItemData[] userDataList, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - using (var connection = GetConnection()) - { - connection.RunInTransaction(db => - { - foreach (var userItemData in userDataList) - { - SaveUserData(db, internalUserId, userItemData.Key, userItemData); - } - }, TransactionMode); - } - } - - /// - /// Gets the user data. - /// - /// The user id. - /// The key. - /// Task{UserItemData}. - /// - /// userId - /// or - /// key - /// - public UserItemData GetUserData(long internalUserId, string key) - { - if (internalUserId <= 0) - { - throw new ArgumentNullException(nameof(internalUserId)); - } - - if (string.IsNullOrEmpty(key)) - { - throw new ArgumentNullException(nameof(key)); - } - - using (var connection = GetConnection(true)) - { - using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId")) - { - statement.TryBind("@UserId", internalUserId); - statement.TryBind("@Key", key); - - foreach (var row in statement.ExecuteQuery()) - { - return ReadRow(row); - } - } - - return null; - } - } - - public UserItemData GetUserData(long internalUserId, List keys) - { - if (keys == null) - { - throw new ArgumentNullException(nameof(keys)); - } - - if (keys.Count == 0) - { - return null; - } - - return GetUserData(internalUserId, keys[0]); - } - - /// - /// Return all user-data associated with the given user - /// - /// - /// - public List GetAllUserData(long internalUserId) - { - if (internalUserId <= 0) - { - throw new ArgumentNullException(nameof(internalUserId)); - } - - var list = new List(); - - using (var connection = GetConnection()) - { - using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId")) - { - statement.TryBind("@UserId", internalUserId); - - foreach (var row in statement.ExecuteQuery()) - { - list.Add(ReadRow(row)); - } - } - } - - return list; - } - - /// - /// Read a row from the specified reader into the provided userData object - /// - /// - private UserItemData ReadRow(IReadOnlyList reader) - { - var userData = new UserItemData(); - - userData.Key = reader[0].ToString(); - //userData.UserId = reader[1].ReadGuidFromBlob(); - - if (reader[2].SQLiteType != SQLiteType.Null) - { - userData.Rating = reader[2].ToDouble(); - } - - userData.Played = reader[3].ToBool(); - userData.PlayCount = reader[4].ToInt(); - userData.IsFavorite = reader[5].ToBool(); - userData.PlaybackPositionTicks = reader[6].ToInt64(); - - if (reader[7].SQLiteType != SQLiteType.Null) - { - userData.LastPlayedDate = reader[7].TryReadDateTime(); - } - - if (reader[8].SQLiteType != SQLiteType.Null) - { - userData.AudioStreamIndex = reader[8].ToInt(); - } - - if (reader[9].SQLiteType != SQLiteType.Null) - { - userData.SubtitleStreamIndex = reader[9].ToInt(); - } - - return userData; - } - } -} diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs deleted file mode 100644 index 0c3f26974..000000000 --- a/Emby.Server.Implementations/Data/SqliteUserRepository.cs +++ /dev/null @@ -1,240 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.IO; -using System.Text.Json; -using MediaBrowser.Common.Json; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Persistence; -using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; - -namespace Emby.Server.Implementations.Data -{ - /// - /// Class SQLiteUserRepository - /// - public class SqliteUserRepository : BaseSqliteRepository, IUserRepository - { - private readonly JsonSerializerOptions _jsonOptions; - - public SqliteUserRepository( - ILogger logger, - IServerApplicationPaths appPaths) - : base(logger) - { - _jsonOptions = JsonDefaults.GetOptions(); - - DbFilePath = Path.Combine(appPaths.DataPath, "users.db"); - } - - /// - /// Gets the name of the repository - /// - /// The name. - public string Name => "SQLite"; - - /// - /// Opens the connection to the database. - /// - public void Initialize() - { - using (var connection = GetConnection()) - { - var localUsersTableExists = TableExists(connection, "LocalUsersv2"); - - connection.RunQueries(new[] { - "create table if not exists LocalUsersv2 (Id INTEGER PRIMARY KEY, guid GUID NOT NULL, data BLOB NOT NULL)", - "drop index if exists idx_users" - }); - - if (!localUsersTableExists && TableExists(connection, "Users")) - { - TryMigrateToLocalUsersTable(connection); - } - - RemoveEmptyPasswordHashes(connection); - } - } - - private void TryMigrateToLocalUsersTable(ManagedConnection connection) - { - try - { - connection.RunQueries(new[] - { - "INSERT INTO LocalUsersv2 (guid, data) SELECT guid,data from users" - }); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error migrating users database"); - } - } - - private void RemoveEmptyPasswordHashes(ManagedConnection connection) - { - foreach (var user in RetrieveAllUsers(connection)) - { - // If the user password is the sha1 hash of the empty string, remove it - if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal) - && !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)) - { - continue; - } - - user.Password = null; - var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions); - - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId")) - { - statement.TryBind("@InternalId", user.InternalId); - statement.TryBind("@data", serialized); - statement.MoveNext(); - } - }, TransactionMode); - } - } - - /// - /// Save a user in the repo - /// - public void CreateUser(User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions); - - using (var connection = GetConnection()) - { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("insert into LocalUsersv2 (guid, data) values (@guid, @data)")) - { - statement.TryBind("@guid", user.Id.ToByteArray()); - statement.TryBind("@data", serialized); - - statement.MoveNext(); - } - - var createdUser = GetUser(user.Id, connection); - - if (createdUser == null) - { - throw new ApplicationException("created user should never be null"); - } - - user.InternalId = createdUser.InternalId; - - }, TransactionMode); - } - } - - public void UpdateUser(User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions); - - using (var connection = GetConnection()) - { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId")) - { - statement.TryBind("@InternalId", user.InternalId); - statement.TryBind("@data", serialized); - statement.MoveNext(); - } - - }, TransactionMode); - } - } - - private User GetUser(Guid guid, ManagedConnection connection) - { - using (var statement = connection.PrepareStatement("select id,guid,data from LocalUsersv2 where guid=@guid")) - { - statement.TryBind("@guid", guid); - - foreach (var row in statement.ExecuteQuery()) - { - return GetUser(row); - } - } - - return null; - } - - private User GetUser(IReadOnlyList row) - { - var id = row[0].ToInt64(); - var guid = row[1].ReadGuidFromBlob(); - - var user = JsonSerializer.Deserialize(row[2].ToBlob(), _jsonOptions); - user.InternalId = id; - user.Id = guid; - return user; - } - - /// - /// Retrieve all users from the database - /// - /// IEnumerable{User}. - public List RetrieveAllUsers() - { - using (var connection = GetConnection(true)) - { - return new List(RetrieveAllUsers(connection)); - } - } - - /// - /// Retrieve all users from the database - /// - /// IEnumerable{User}. - private IEnumerable RetrieveAllUsers(ManagedConnection connection) - { - foreach (var row in connection.Query("select id,guid,data from LocalUsersv2")) - { - yield return GetUser(row); - } - } - - /// - /// Deletes the user. - /// - /// The user. - /// Task. - /// user - public void DeleteUser(User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - using (var connection = GetConnection()) - { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("delete from LocalUsersv2 where Id=@id")) - { - statement.TryBind("@id", user.InternalId); - statement.MoveNext(); - } - }, TransactionMode); - } - } - } -} diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs index 579cb895e..d6bce93f8 100644 --- a/Emby.Server.Implementations/Devices/DeviceManager.cs +++ b/Emby.Server.Implementations/Devices/DeviceManager.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; @@ -360,7 +361,7 @@ namespace Emby.Server.Implementations.Devices private string DefaultCameraUploadsPath => Path.Combine(_config.CommonApplicationPaths.DataPath, "camerauploads"); - public bool CanAccessDevice(User user, string deviceId) + public bool CanAccessDevice(Jellyfin.Data.Entities.User user, string deviceId) { if (user == null) { @@ -371,7 +372,13 @@ namespace Emby.Server.Implementations.Devices throw new ArgumentNullException(nameof(deviceId)); } - if (!CanAccessDevice(user.Policy, deviceId)) + if (user.HasPermission(PermissionKind.EnableAllDevices) + || user.HasPermission(PermissionKind.IsAdministrator)) + { + return true; + } + + if (!user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparer.OrdinalIgnoreCase)) { var capabilities = GetCapabilities(deviceId); @@ -383,21 +390,6 @@ namespace Emby.Server.Implementations.Devices return true; } - - private static bool CanAccessDevice(UserPolicy policy, string id) - { - if (policy.EnableAllDevices) - { - return true; - } - - if (policy.IsAdministrator) - { - return true; - } - - return policy.EnabledDevices.Contains(id, StringComparer.OrdinalIgnoreCase); - } } public class DeviceManagerEntryPoint : IServerEntryPoint diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index c4b65d265..da40bca4c 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Common; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Drawing; @@ -74,7 +75,7 @@ namespace Emby.Server.Implementations.Dto /// The owner. /// Task{DtoBaseItem}. /// item - public BaseItemDto GetBaseItemDto(BaseItem item, ItemFields[] fields, User user = null, BaseItem owner = null) + public BaseItemDto GetBaseItemDto(BaseItem item, ItemFields[] fields, Jellyfin.Data.Entities.User user = null, BaseItem owner = null) { var options = new DtoOptions { @@ -85,7 +86,7 @@ namespace Emby.Server.Implementations.Dto } /// - public IReadOnlyList GetBaseItemDtos(IReadOnlyList items, DtoOptions options, User user = null, BaseItem owner = null) + public IReadOnlyList GetBaseItemDtos(IReadOnlyList items, DtoOptions options, Jellyfin.Data.Entities.User user = null, BaseItem owner = null) { var returnItems = new BaseItemDto[items.Count]; var programTuples = new List<(BaseItem, BaseItemDto)>(); @@ -138,7 +139,7 @@ namespace Emby.Server.Implementations.Dto return returnItems; } - public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null) + public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, Jellyfin.Data.Entities.User user = null, BaseItem owner = null) { var dto = GetBaseItemDtoInternal(item, options, user, owner); if (item is LiveTvChannel tvChannel) @@ -172,7 +173,7 @@ namespace Emby.Server.Implementations.Dto return dto; } - private static IList GetTaggedItems(IItemByName byName, User user, DtoOptions options) + private static IList GetTaggedItems(IItemByName byName, Jellyfin.Data.Entities.User user, DtoOptions options) { return byName.GetTaggedItems( new InternalItemsQuery(user) @@ -182,7 +183,7 @@ namespace Emby.Server.Implementations.Dto }); } - private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null) + private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, Jellyfin.Data.Entities.User user = null, BaseItem owner = null) { var dto = new BaseItemDto { @@ -315,7 +316,7 @@ namespace Emby.Server.Implementations.Dto } } - public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List taggedItems, User user = null) + public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List taggedItems, Jellyfin.Data.Entities.User user = null) { var dto = GetBaseItemDtoInternal(item, options, user); @@ -327,7 +328,7 @@ namespace Emby.Server.Implementations.Dto return dto; } - private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IList taggedItems, User user = null) + private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IList taggedItems, Jellyfin.Data.Entities.User user = null) { if (item is MusicArtist) { @@ -363,7 +364,7 @@ namespace Emby.Server.Implementations.Dto /// /// Attaches the user specific info. /// - private void AttachUserSpecificInfo(BaseItemDto dto, BaseItem item, User user, DtoOptions options) + private void AttachUserSpecificInfo(BaseItemDto dto, BaseItem item, Jellyfin.Data.Entities.User user, DtoOptions options) { if (item.IsFolder) { @@ -384,7 +385,7 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.ChildCount)) { - dto.ChildCount = dto.ChildCount ?? GetChildCount(folder, user); + dto.ChildCount ??= GetChildCount(folder, user); } } @@ -414,7 +415,7 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.BasicSyncInfo)) { - var userCanSync = user != null && user.Policy.EnableContentDownloading; + var userCanSync = user != null && user.HasPermission(PermissionKind.EnableContentDownloading); if (userCanSync && item.SupportsExternalTransfer) { dto.SupportsSync = true; @@ -422,7 +423,7 @@ namespace Emby.Server.Implementations.Dto } } - private static int GetChildCount(Folder folder, User user) + private static int GetChildCount(Folder folder, Jellyfin.Data.Entities.User user) { // Right now this is too slow to calculate for top level folders on a per-user basis // Just return something so that apps that are expecting a value won't think the folders are empty diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 8e3236407..7ece52cad 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -446,7 +446,7 @@ namespace Emby.Server.Implementations.EntryPoints /// The user. /// if set to true [include if not found]. /// IEnumerable{``0}. - private IEnumerable TranslatePhysicalItemToUserLibrary(T item, User user, bool includeIfNotFound = false) + private IEnumerable TranslatePhysicalItemToUserLibrary(T item, Jellyfin.Data.Entities.User user, bool includeIfNotFound = false) where T : BaseItem { // If the physical root changed, return the user root diff --git a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs index 41c0c5115..75dde5598 100644 --- a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Plugins; @@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.EntryPoints private async void SendMessage(string name, TimerEventInfo info) { - var users = _userManager.Users.Where(i => i.Policy.EnableLiveTvAccess).Select(i => i.Id).ToList(); + var users = _userManager.Users.Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess)).Select(i => i.Id).ToList(); try { diff --git a/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs b/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs deleted file mode 100644 index 54f4b67e6..000000000 --- a/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Tasks; - -namespace Emby.Server.Implementations.EntryPoints -{ - /// - /// Class RefreshUsersMetadata. - /// - public class RefreshUsersMetadata : IScheduledTask, IConfigurableScheduledTask - { - /// - /// The user manager. - /// - private readonly IUserManager _userManager; - private readonly IFileSystem _fileSystem; - - /// - /// Initializes a new instance of the class. - /// - public RefreshUsersMetadata(IUserManager userManager, IFileSystem fileSystem) - { - _userManager = userManager; - _fileSystem = fileSystem; - } - - /// - public string Name => "Refresh Users"; - - /// - public string Key => "RefreshUsers"; - - /// - public string Description => "Refresh user infos"; - - /// - public string Category => "Library"; - - /// - public bool IsHidden => true; - - /// - public bool IsEnabled => true; - - /// - public bool IsLogged => true; - - /// - public async Task Execute(CancellationToken cancellationToken, IProgress progress) - { - foreach (var user in _userManager.Users) - { - cancellationToken.ThrowIfCancellationRequested(); - - await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false); - } - } - - /// - public IEnumerable GetDefaultTriggers() - { - return new[] - { - new TaskTriggerInfo - { - IntervalTicks = TimeSpan.FromDays(1).Ticks, - Type = TaskTriggerInfo.TriggerInterval - } - }; - } - } -} diff --git a/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs b/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs index e1dbb663b..436d723f0 100644 --- a/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs @@ -3,10 +3,10 @@ using System.Collections.Generic; using System.Globalization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; using MediaBrowser.Controller; -using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; @@ -67,10 +67,8 @@ namespace Emby.Server.Implementations.EntryPoints /// public Task RunAsync() { - _userManager.UserDeleted += OnUserDeleted; - _userManager.UserUpdated += OnUserUpdated; - _userManager.UserPolicyUpdated += OnUserPolicyUpdated; - _userManager.UserConfigurationUpdated += OnUserConfigurationUpdated; + _userManager.OnUserDeleted += OnUserDeleted; + _userManager.OnUserUpdated += OnUserUpdated; _appHost.HasPendingRestartChanged += OnHasPendingRestartChanged; @@ -152,20 +150,6 @@ namespace Emby.Server.Implementations.EntryPoints SendMessageToUserSession(e.Argument, "UserDeleted", e.Argument.Id.ToString("N", CultureInfo.InvariantCulture)); } - private void OnUserPolicyUpdated(object sender, GenericEventArgs e) - { - var dto = _userManager.GetUserDto(e.Argument); - - SendMessageToUserSession(e.Argument, "UserPolicyUpdated", dto); - } - - private void OnUserConfigurationUpdated(object sender, GenericEventArgs e) - { - var dto = _userManager.GetUserDto(e.Argument); - - SendMessageToUserSession(e.Argument, "UserConfigurationUpdated", dto); - } - private async void SendMessageToAdminSessions(string name, T data) { try @@ -209,10 +193,9 @@ namespace Emby.Server.Implementations.EntryPoints { if (dispose) { - _userManager.UserDeleted -= OnUserDeleted; - _userManager.UserUpdated -= OnUserUpdated; - _userManager.UserPolicyUpdated -= OnUserPolicyUpdated; - _userManager.UserConfigurationUpdated -= OnUserConfigurationUpdated; + _userManager.OnUserDeleted -= OnUserDeleted; + _userManager.OnUserUpdated -= OnUserUpdated; + _installationManager.PluginUninstalled -= OnPluginUninstalled; _installationManager.PackageInstalling -= OnPackageInstalling; diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index 256b24924..ad7b76d4f 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Security.Authentication; using Emby.Server.Implementations.SocketSharp; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -43,14 +44,14 @@ namespace Emby.Server.Implementations.HttpServer.Security ValidateUser(request, authAttribtues); } - public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes) + public Jellyfin.Data.Entities.User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes) { var req = new WebSocketSharpRequest(request, null, request.Path, _logger); var user = ValidateUser(req, authAttributes); return user; } - private User ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues) + private Jellyfin.Data.Entities.User ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues) { // This code is executed before the service var auth = _authorizationContext.GetAuthorizationInfo(request); @@ -90,7 +91,8 @@ namespace Emby.Server.Implementations.HttpServer.Security !string.IsNullOrEmpty(auth.Client) && !string.IsNullOrEmpty(auth.Device)) { - _sessionManager.LogSessionActivity(auth.Client, + _sessionManager.LogSessionActivity( + auth.Client, auth.Version, auth.DeviceId, auth.Device, @@ -102,22 +104,22 @@ namespace Emby.Server.Implementations.HttpServer.Security } private void ValidateUserAccess( - User user, + Jellyfin.Data.Entities.User user, IRequest request, IAuthenticationAttributes authAttribtues, AuthorizationInfo auth) { - if (user.Policy.IsDisabled) + if (user.HasPermission(PermissionKind.IsDisabled)) { throw new SecurityException("User account has been disabled."); } - if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(request.RemoteIp)) + if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !_networkManager.IsInLocalNetwork(request.RemoteIp)) { throw new SecurityException("User account has been disabled."); } - if (!user.Policy.IsAdministrator + if (!user.HasPermission(PermissionKind.IsAdministrator) && !authAttribtues.EscapeParentalControl && !user.IsParentalScheduleAllowed()) { @@ -176,11 +178,11 @@ namespace Emby.Server.Implementations.HttpServer.Security return false; } - private static void ValidateRoles(string[] roles, User user) + private static void ValidateRoles(string[] roles, Jellyfin.Data.Entities.User user) { if (roles.Contains("admin", StringComparer.OrdinalIgnoreCase)) { - if (user == null || !user.Policy.IsAdministrator) + if (user == null || !user.HasPermission(PermissionKind.IsAdministrator)) { throw new SecurityException("User does not have admin access."); } @@ -188,7 +190,7 @@ namespace Emby.Server.Implementations.HttpServer.Security if (roles.Contains("delete", StringComparer.OrdinalIgnoreCase)) { - if (user == null || !user.Policy.EnableContentDeletion) + if (user == null || !user.HasPermission(PermissionKind.EnableContentDeletion)) { throw new SecurityException("User does not have delete access."); } @@ -196,7 +198,7 @@ namespace Emby.Server.Implementations.HttpServer.Security if (roles.Contains("download", StringComparer.OrdinalIgnoreCase)) { - if (user == null || !user.Policy.EnableContentDownloading) + if (user == null || !user.HasPermission(PermissionKind.EnableContentDownloading)) { throw new SecurityException("User does not have download access."); } diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs index 129faeaab..9558cb4c6 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -149,9 +149,9 @@ namespace Emby.Server.Implementations.HttpServer.Security { info.User = _userManager.GetUserById(tokenInfo.UserId); - if (info.User != null && !string.Equals(info.User.Name, tokenInfo.UserName, StringComparison.OrdinalIgnoreCase)) + if (info.User != null && !string.Equals(info.User.Username, tokenInfo.UserName, StringComparison.OrdinalIgnoreCase)) { - tokenInfo.UserName = info.User.Name; + tokenInfo.UserName = info.User.Username; updateToken = true; } } diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs index 166952c64..3f8a64f99 100644 --- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs @@ -42,14 +42,14 @@ namespace Emby.Server.Implementations.HttpServer.Security return GetSession((IRequest)requestContext); } - public User GetUser(IRequest requestContext) + public Jellyfin.Data.Entities.User GetUser(IRequest requestContext) { var session = GetSession(requestContext); return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId); } - public User GetUser(object requestContext) + public Jellyfin.Data.Entities.User GetUser(object requestContext) { return GetUser((IRequest)requestContext); } diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs deleted file mode 100644 index 52c8facc3..000000000 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ /dev/null @@ -1,177 +0,0 @@ -using System; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using MediaBrowser.Common; -using MediaBrowser.Common.Cryptography; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Cryptography; - -namespace Emby.Server.Implementations.Library -{ - /// - /// The default authentication provider. - /// - public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser - { - private readonly ICryptoProvider _cryptographyProvider; - - /// - /// Initializes a new instance of the class. - /// - /// The cryptography provider. - public DefaultAuthenticationProvider(ICryptoProvider cryptographyProvider) - { - _cryptographyProvider = cryptographyProvider; - } - - /// - public string Name => "Default"; - - /// - public bool IsEnabled => true; - - /// - // This is dumb and an artifact of the backwards way auth providers were designed. - // This version of authenticate was never meant to be called, but needs to be here for interface compat - // Only the providers that don't provide local user support use this - public Task Authenticate(string username, string password) - { - throw new NotImplementedException(); - } - - /// - // This is the version that we need to use for local users. Because reasons. - public Task Authenticate(string username, string password, User resolvedUser) - { - if (resolvedUser == null) - { - throw new AuthenticationException($"Specified user does not exist."); - } - - bool success = false; - - // As long as jellyfin supports passwordless users, we need this little block here to accommodate - if (!HasPassword(resolvedUser) && string.IsNullOrEmpty(password)) - { - return Task.FromResult(new ProviderAuthenticationResult - { - Username = username - }); - } - - byte[] passwordbytes = Encoding.UTF8.GetBytes(password); - - PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password); - if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id) - || _cryptographyProvider.DefaultHashMethod == readyHash.Id) - { - byte[] calculatedHash = _cryptographyProvider.ComputeHash( - readyHash.Id, - passwordbytes, - readyHash.Salt.ToArray()); - - if (readyHash.Hash.SequenceEqual(calculatedHash)) - { - success = true; - } - } - else - { - throw new AuthenticationException($"Requested crypto method not available in provider: {readyHash.Id}"); - } - - if (!success) - { - throw new AuthenticationException("Invalid username or password"); - } - - return Task.FromResult(new ProviderAuthenticationResult - { - Username = username - }); - } - - /// - public bool HasPassword(User user) - => !string.IsNullOrEmpty(user.Password); - - /// - public Task ChangePassword(User user, string newPassword) - { - if (string.IsNullOrEmpty(newPassword)) - { - user.Password = null; - return Task.CompletedTask; - } - - PasswordHash newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword); - user.Password = newPasswordHash.ToString(); - - return Task.CompletedTask; - } - - /// - public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) - { - if (newPassword != null) - { - newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword).ToString(); - } - - if (string.IsNullOrWhiteSpace(newPasswordHash)) - { - throw new ArgumentNullException(nameof(newPasswordHash)); - } - - user.EasyPassword = newPasswordHash; - } - - /// - public string GetEasyPasswordHash(User user) - { - return string.IsNullOrEmpty(user.EasyPassword) - ? null - : Hex.Encode(PasswordHash.Parse(user.EasyPassword).Hash); - } - - /// - /// Gets the hashed string. - /// - public string GetHashedString(User user, string str) - { - if (string.IsNullOrEmpty(user.Password)) - { - return _cryptographyProvider.CreatePasswordHash(str).ToString(); - } - - // TODO: make use of iterations parameter? - PasswordHash passwordHash = PasswordHash.Parse(user.Password); - var salt = passwordHash.Salt.ToArray(); - return new PasswordHash( - passwordHash.Id, - _cryptographyProvider.ComputeHash( - passwordHash.Id, - Encoding.UTF8.GetBytes(str), - salt), - salt, - passwordHash.Parameters.ToDictionary(x => x.Key, y => y.Value)).ToString(); - } - - public ReadOnlySpan GetHashed(User user, string str) - { - if (string.IsNullOrEmpty(user.Password)) - { - return _cryptographyProvider.CreatePasswordHash(str).Hash; - } - - // TODO: make use of iterations parameter? - PasswordHash passwordHash = PasswordHash.Parse(user.Password); - return _cryptographyProvider.ComputeHash( - passwordHash.Id, - Encoding.UTF8.GetBytes(str), - passwordHash.Salt.ToArray()); - } - } -} diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs deleted file mode 100644 index 6c6fbd86f..000000000 --- a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Security.Cryptography; -using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; - -namespace Emby.Server.Implementations.Library -{ - /// - /// The default password reset provider. - /// - public class DefaultPasswordResetProvider : IPasswordResetProvider - { - private const string BaseResetFileName = "passwordreset"; - - private readonly IJsonSerializer _jsonSerializer; - private readonly IUserManager _userManager; - - private readonly string _passwordResetFileBase; - private readonly string _passwordResetFileBaseDir; - - /// - /// Initializes a new instance of the class. - /// - /// The configuration manager. - /// The JSON serializer. - /// The user manager. - public DefaultPasswordResetProvider( - IServerConfigurationManager configurationManager, - IJsonSerializer jsonSerializer, - IUserManager userManager) - { - _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath; - _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName); - _jsonSerializer = jsonSerializer; - _userManager = userManager; - } - - /// - public string Name => "Default Password Reset Provider"; - - /// - public bool IsEnabled => true; - - /// - public async Task RedeemPasswordResetPin(string pin) - { - SerializablePasswordReset spr; - List usersreset = new List(); - foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*")) - { - using (var str = File.OpenRead(resetfile)) - { - spr = await _jsonSerializer.DeserializeFromStreamAsync(str).ConfigureAwait(false); - } - - if (spr.ExpirationDate < DateTime.Now) - { - File.Delete(resetfile); - } - else if (string.Equals( - spr.Pin.Replace("-", string.Empty, StringComparison.Ordinal), - pin.Replace("-", string.Empty, StringComparison.Ordinal), - StringComparison.InvariantCultureIgnoreCase)) - { - var resetUser = _userManager.GetUserByName(spr.UserName); - if (resetUser == null) - { - throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found"); - } - - await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); - usersreset.Add(resetUser.Name); - File.Delete(resetfile); - } - } - - if (usersreset.Count < 1) - { - throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}"); - } - else - { - return new PinRedeemResult - { - Success = true, - UsersReset = usersreset.ToArray() - }; - } - } - - /// - public async Task StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork) - { - string pin = string.Empty; - using (var cryptoRandom = RandomNumberGenerator.Create()) - { - byte[] bytes = new byte[4]; - cryptoRandom.GetBytes(bytes); - pin = BitConverter.ToString(bytes); - } - - DateTime expireTime = DateTime.Now.AddMinutes(30); - string filePath = _passwordResetFileBase + user.InternalId + ".json"; - SerializablePasswordReset spr = new SerializablePasswordReset - { - ExpirationDate = expireTime, - Pin = pin, - PinFile = filePath, - UserName = user.Name - }; - - using (FileStream fileStream = File.OpenWrite(filePath)) - { - _jsonSerializer.SerializeToStream(spr, fileStream); - await fileStream.FlushAsync().ConfigureAwait(false); - } - - return new ForgotPasswordResult - { - Action = ForgotPasswordAction.PinCode, - PinExpirationDate = expireTime, - PinFile = filePath - }; - } - - private class SerializablePasswordReset : PasswordPinCreationResult - { - public string Pin { get; set; } - - public string UserName { get; set; } - } - } -} diff --git a/Emby.Server.Implementations/Library/InvalidAuthProvider.cs b/Emby.Server.Implementations/Library/InvalidAuthProvider.cs deleted file mode 100644 index dc61aacd7..000000000 --- a/Emby.Server.Implementations/Library/InvalidAuthProvider.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Entities; - -namespace Emby.Server.Implementations.Library -{ - /// - /// An invalid authentication provider. - /// - public class InvalidAuthProvider : IAuthenticationProvider - { - /// - public string Name => "InvalidOrMissingAuthenticationProvider"; - - /// - public bool IsEnabled => true; - - /// - public Task Authenticate(string username, string password) - { - throw new AuthenticationException("User Account cannot login with this provider. The Normal provider for this user cannot be found"); - } - - /// - public bool HasPassword(User user) - { - return true; - } - - /// - public Task ChangePassword(User user, string newPassword) - { - return Task.CompletedTask; - } - - /// - public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) - { - // Nothing here - } - - /// - public string GetPasswordHash(User user) - { - return string.Empty; - } - - /// - public string GetEasyPasswordHash(User user) - { - return string.Empty; - } - } -} diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 0b86b2db7..e7cd7512c 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -17,6 +17,7 @@ using Emby.Server.Implementations.Library.Resolvers; using Emby.Server.Implementations.Library.Validators; using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.ScheduledTasks; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; using MediaBrowser.Controller; @@ -1470,7 +1471,7 @@ namespace Emby.Server.Implementations.Library query.Parent = null; } - private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true) + private void AddUserToQuery(InternalItemsQuery query, Jellyfin.Data.Entities.User user, bool allowExternalContent = true) { if (query.AncestorIds.Length == 0 && query.ParentId.Equals(Guid.Empty) && @@ -1491,7 +1492,7 @@ namespace Emby.Server.Implementations.Library } } - private IEnumerable GetTopParentIdsForQuery(BaseItem item, User user) + private IEnumerable GetTopParentIdsForQuery(BaseItem item, Jellyfin.Data.Entities.User user) { if (item is UserView view) { @@ -1524,7 +1525,8 @@ namespace Emby.Server.Implementations.Library } // Handle grouping - if (user != null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType) && user.Configuration.GroupedFolders.Length > 0) + if (user != null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType) + && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0) { return GetUserRootFolder() .GetChildren(user, true) @@ -1557,7 +1559,7 @@ namespace Emby.Server.Implementations.Library /// The item. /// The user. /// IEnumerable{System.String}. - public async Task> GetIntros(BaseItem item, User user) + public async Task> GetIntros(BaseItem item, Jellyfin.Data.Entities.User user) { var tasks = IntroProviders .OrderBy(i => i.GetType().Name.Contains("Default", StringComparison.OrdinalIgnoreCase) ? 1 : 0) @@ -1579,7 +1581,7 @@ namespace Emby.Server.Implementations.Library /// The item. /// The user. /// Task<IEnumerable<IntroInfo>>. - private async Task> GetIntros(IIntroProvider provider, BaseItem item, User user) + private async Task> GetIntros(IIntroProvider provider, BaseItem item, Jellyfin.Data.Entities.User user) { try { @@ -1680,7 +1682,7 @@ namespace Emby.Server.Implementations.Library /// The sort by. /// The sort order. /// IEnumerable{BaseItem}. - public IEnumerable Sort(IEnumerable items, User user, IEnumerable sortBy, SortOrder sortOrder) + public IEnumerable Sort(IEnumerable items, Jellyfin.Data.Entities.User user, IEnumerable sortBy, SortOrder sortOrder) { var isFirst = true; @@ -1703,7 +1705,7 @@ namespace Emby.Server.Implementations.Library return orderedItems ?? items; } - public IEnumerable Sort(IEnumerable items, User user, IEnumerable> orderByList) + public IEnumerable Sort(IEnumerable items, Jellyfin.Data.Entities.User user, IEnumerable> orderByList) { var isFirst = true; @@ -1740,7 +1742,7 @@ namespace Emby.Server.Implementations.Library /// The name. /// The user. /// IBaseItemComparer. - private IBaseItemComparer GetComparer(string name, User user) + private IBaseItemComparer GetComparer(string name, Jellyfin.Data.Entities.User user) { var comparer = Comparers.FirstOrDefault(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase)); @@ -2072,7 +2074,7 @@ namespace Emby.Server.Implementations.Library private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24); public UserView GetNamedView( - User user, + Jellyfin.Data.Entities.User user, string name, string viewType, string sortName) @@ -2125,7 +2127,7 @@ namespace Emby.Server.Implementations.Library } public UserView GetNamedView( - User user, + Jellyfin.Data.Entities.User user, string name, Guid parentId, string viewType, diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 01fe98f3a..25af69058 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Entities; @@ -14,7 +15,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -145,7 +145,7 @@ namespace Emby.Server.Implementations.Library }); } - public async Task> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken) + public async Task> GetPlaybackMediaSources(BaseItem item, Jellyfin.Data.Entities.User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken) { var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user); @@ -190,10 +190,7 @@ namespace Emby.Server.Implementations.Library { if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) { - if (!user.Policy.EnableAudioPlaybackTranscoding) - { - source.SupportsTranscoding = false; - } + source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding); } } } @@ -312,7 +309,7 @@ namespace Emby.Server.Implementations.Library return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase)); } - public List GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null) + public List GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, Jellyfin.Data.Entities.User user = null) { if (item == null) { @@ -350,9 +347,11 @@ namespace Emby.Server.Implementations.Library return new string[] { language }; } - private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection) + private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, Jellyfin.Data.Entities.User user, bool allowRememberingSelection) { - if (userData.SubtitleStreamIndex.HasValue && user.Configuration.RememberSubtitleSelections && user.Configuration.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection) + if (userData.SubtitleStreamIndex.HasValue + && user.RememberSubtitleSelections + && user.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection) { var index = userData.SubtitleStreamIndex.Value; // Make sure the saved index is still valid @@ -363,26 +362,27 @@ namespace Emby.Server.Implementations.Library } } - var preferredSubs = string.IsNullOrEmpty(user.Configuration.SubtitleLanguagePreference) - ? Array.Empty() : NormalizeLanguage(user.Configuration.SubtitleLanguagePreference); + + var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference) + ? Array.Empty() : NormalizeLanguage(user.SubtitleLanguagePreference); var defaultAudioIndex = source.DefaultAudioStreamIndex; var audioLangage = defaultAudioIndex == null ? null : source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select(i => i.Language).FirstOrDefault(); - source.DefaultSubtitleStreamIndex = MediaStreamSelector.GetDefaultSubtitleStreamIndex(source.MediaStreams, + source.DefaultSubtitleStreamIndex = MediaStreamSelector.GetDefaultSubtitleStreamIndex( + source.MediaStreams, preferredSubs, - user.Configuration.SubtitleMode, + user.SubtitleMode, audioLangage); - MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, - user.Configuration.SubtitleMode, audioLangage); + MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLangage); } - private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection) + private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, Jellyfin.Data.Entities.User user, bool allowRememberingSelection) { - if (userData.AudioStreamIndex.HasValue && user.Configuration.RememberAudioSelections && allowRememberingSelection) + if (userData.AudioStreamIndex.HasValue && user.RememberAudioSelections && allowRememberingSelection) { var index = userData.AudioStreamIndex.Value; // Make sure the saved index is still valid @@ -393,14 +393,14 @@ namespace Emby.Server.Implementations.Library } } - var preferredAudio = string.IsNullOrEmpty(user.Configuration.AudioLanguagePreference) + var preferredAudio = string.IsNullOrEmpty(user.AudioLanguagePreference) ? Array.Empty() - : NormalizeLanguage(user.Configuration.AudioLanguagePreference); + : NormalizeLanguage(user.AudioLanguagePreference); - source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.Configuration.PlayDefaultAudioTrack); + source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack); } - public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user) + public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, Jellyfin.Data.Entities.User user) { // Item would only be null if the app didn't supply ItemId as part of the live stream open request var mediaType = item == null ? MediaType.Video : item.MediaType; @@ -560,17 +560,14 @@ namespace Emby.Server.Implementations.Library { videoStream.BitRate = 30000000; } - else if (width >= 1900) { videoStream.BitRate = 20000000; } - else if (width >= 1200) { videoStream.BitRate = 8000000; } - else if (width >= 700) { videoStream.BitRate = 2000000; @@ -674,13 +671,14 @@ namespace Emby.Server.Implementations.Library mediaSource.AnalyzeDurationMs = 3000; } - mediaInfo = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest + mediaInfo = await _mediaEncoder.GetMediaInfo( + new MediaInfoRequest { MediaSource = mediaSource, MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video, ExtractChapters = false - - }, cancellationToken).ConfigureAwait(false); + }, + cancellationToken).ConfigureAwait(false); if (cacheFilePath != null) { diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs index e27145a1d..a177138b7 100644 --- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs +++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index 1ec578371..ad8c70f5e 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -22,7 +22,7 @@ namespace Emby.Server.Implementations.Library _libraryManager = libraryManager; } - public List GetInstantMixFromSong(Audio item, User user, DtoOptions dtoOptions) + public List GetInstantMixFromSong(Audio item, Jellyfin.Data.Entities.User user, DtoOptions dtoOptions) { var list = new List /// The user object. /// The item. - private void OnPlaybackStart(User user, BaseItem item) + private void OnPlaybackStart(Jellyfin.Data.Entities.User user, BaseItem item) { var data = _userDataManager.GetUserData(user, item); @@ -754,7 +764,7 @@ namespace Emby.Server.Implementations.Session StartIdleCheckTimer(); } - private void OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info) + private void OnPlaybackProgress(Jellyfin.Data.Entities.User user, BaseItem item, PlaybackProgressInfo info) { var data = _userDataManager.GetUserData(user, item); @@ -780,11 +790,11 @@ namespace Emby.Server.Implementations.Session } } - private static bool UpdatePlaybackSettings(User user, PlaybackProgressInfo info, UserItemData data) + private static bool UpdatePlaybackSettings(Jellyfin.Data.Entities.User user, PlaybackProgressInfo info, UserItemData data) { var changed = false; - if (user.Configuration.RememberAudioSelections) + if (user.RememberAudioSelections) { if (data.AudioStreamIndex != info.AudioStreamIndex) { @@ -801,7 +811,7 @@ namespace Emby.Server.Implementations.Session } } - if (user.Configuration.RememberSubtitleSelections) + if (user.RememberSubtitleSelections) { if (data.SubtitleStreamIndex != info.SubtitleStreamIndex) { @@ -940,7 +950,7 @@ namespace Emby.Server.Implementations.Session _logger); } - private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed) + private bool OnPlaybackStopped(Jellyfin.Data.Entities.User user, BaseItem item, long? positionTicks, bool playbackFailed) { bool playedToCompletion = false; @@ -1112,13 +1122,13 @@ namespace Emby.Server.Implementations.Session if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full)) { throw new ArgumentException( - string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Name)); + string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Username)); } } if (user != null && command.ItemIds.Length == 1 - && user.Configuration.EnableNextEpisodeAutoPlay + && user.EnableNextEpisodeAutoPlay && _libraryManager.GetItemById(command.ItemIds[0]) is Episode episode) { var series = episode.Series; @@ -1154,7 +1164,7 @@ namespace Emby.Server.Implementations.Session await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false); } - private IEnumerable TranslateItemForPlayback(Guid id, User user) + private IEnumerable TranslateItemForPlayback(Guid id, Jellyfin.Data.Entities.User user) { var item = _libraryManager.GetItemById(id); @@ -1173,7 +1183,7 @@ namespace Emby.Server.Implementations.Session DtoOptions = new DtoOptions(false) { EnableImages = false, - Fields = new ItemFields[] + Fields = new[] { ItemFields.SortName } @@ -1207,7 +1217,7 @@ namespace Emby.Server.Implementations.Session return new[] { item }; } - private IEnumerable TranslateItemForInstantMix(Guid id, User user) + private IEnumerable TranslateItemForInstantMix(Guid id, Jellyfin.Data.Entities.User user) { var item = _libraryManager.GetItemById(id); @@ -1335,7 +1345,7 @@ namespace Emby.Server.Implementations.Session list.Add(new SessionUserInfo { UserId = userId, - UserName = user.Name + UserName = user.Username }); session.AdditionalUsers = list.ToArray(); @@ -1390,7 +1400,7 @@ namespace Emby.Server.Implementations.Session { CheckDisposed(); - User user = null; + Jellyfin.Data.Entities.User user = null; if (request.UserId != Guid.Empty) { user = _userManager.GetUserById(request.UserId); @@ -1446,7 +1456,7 @@ namespace Emby.Server.Implementations.Session return returnResult; } - private string GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName) + private string GetAuthorizationToken(Jellyfin.Data.Entities.User user, string deviceId, string app, string appVersion, string deviceName) { var existing = _authRepo.Get( new AuthenticationInfoQuery @@ -1495,7 +1505,7 @@ namespace Emby.Server.Implementations.Session DeviceName = deviceName, UserId = user.Id, AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), - UserName = user.Name + UserName = user.Username }; _logger.LogInformation("Creating new access token for user {0}", user.Id); @@ -1692,15 +1702,15 @@ namespace Emby.Server.Implementations.Session return info; } - private string GetImageCacheTag(BaseItem item, ImageType type) + private string GetImageCacheTag(Jellyfin.Data.Entities.User user) { try { - return _imageProcessor.GetImageCacheTag(item, type); + return _imageProcessor.GetImageCacheTag(user); } - catch (Exception ex) + catch (Exception e) { - _logger.LogError(ex, "Error getting image information for {Type}", type); + _logger.LogError(e, "Error getting image information for profile image"); return null; } } @@ -1809,7 +1819,10 @@ namespace Emby.Server.Implementations.Session { CheckDisposed(); - var adminUserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToList(); + var adminUserIds = _userManager.Users + .Where(i => i.HasPermission(PermissionKind.IsAdministrator)) + .Select(i => i.Id) + .ToList(); return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken); } diff --git a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs index 623675157..3cc294371 100644 --- a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs +++ b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs @@ -12,7 +12,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets or sets the user. /// /// The user. - public User User { get; set; } + public Jellyfin.Data.Entities.User User { get; set; } /// /// Gets or sets the user manager. diff --git a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs index 73f59f8cd..57a1a00d9 100644 --- a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs @@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets or sets the user. /// /// The user. - public User User { get; set; } + public Jellyfin.Data.Entities.User User { get; set; } /// /// Gets or sets the user manager. diff --git a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs index 66de05a6a..c9feca7e3 100644 --- a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs @@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets or sets the user. /// /// The user. - public User User { get; set; } + public Jellyfin.Data.Entities.User User { get; set; } /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs index da3f3dd25..6f383e65f 100644 --- a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs @@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets or sets the user. /// /// The user. - public User User { get; set; } + public Jellyfin.Data.Entities.User User { get; set; } /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs index d99d0eff2..4845fdc0d 100644 --- a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs @@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets or sets the user. /// /// The user. - public User User { get; set; } + public Jellyfin.Data.Entities.User User { get; set; } /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs index eb74ce1bd..99846db61 100644 --- a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs +++ b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs @@ -14,7 +14,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets or sets the user. /// /// The user. - public User User { get; set; } + public Jellyfin.Data.Entities.User User { get; set; } /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index 4c2f24e6f..905a1ea99 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -74,7 +75,8 @@ namespace Emby.Server.Implementations.TV { parents = _libraryManager.GetUserRootFolder().GetChildren(user, true) .Where(i => i is Folder) - .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))) + .Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes) + .Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))) .ToArray(); } @@ -137,7 +139,7 @@ namespace Emby.Server.Implementations.TV return GetResult(episodes, request); } - public IEnumerable GetNextUpEpisodes(NextUpQuery request, User user, IEnumerable seriesKeys, DtoOptions dtoOptions) + public IEnumerable GetNextUpEpisodes(NextUpQuery request, Jellyfin.Data.Entities.User user, IEnumerable seriesKeys, DtoOptions dtoOptions) { // Avoid implicitly captured closure var currentUser = user; @@ -186,13 +188,13 @@ namespace Emby.Server.Implementations.TV /// Gets the next up. /// /// Task{Episode}. - private Tuple> GetNextUp(string seriesKey, User user, DtoOptions dtoOptions) + private Tuple> GetNextUp(string seriesKey, Jellyfin.Data.Entities.User user, DtoOptions dtoOptions) { var lastWatchedEpisode = _libraryManager.GetItemList(new InternalItemsQuery(user) { AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, - IncludeItemTypes = new[] { typeof(Episode).Name }, + IncludeItemTypes = new[] { nameof(Episode) }, OrderBy = new[] { new ValueTuple(ItemSortBy.SortName, SortOrder.Descending) }, IsPlayed = true, Limit = 1, @@ -205,7 +207,6 @@ namespace Emby.Server.Implementations.TV }, EnableImages = false } - }).FirstOrDefault(); Func getEpisode = () => @@ -220,7 +221,7 @@ namespace Emby.Server.Implementations.TV IsPlayed = false, IsVirtualItem = false, ParentIndexNumberNotEquals = 0, - MinSortName = lastWatchedEpisode == null ? null : lastWatchedEpisode.SortName, + MinSortName = lastWatchedEpisode?.SortName, DtoOptions = dtoOptions }).Cast().FirstOrDefault(); diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs index 26f7d9d2d..4929e8897 100644 --- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs +++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs @@ -2,6 +2,7 @@ using System.Security.Claims; using System.Text.Encodings.Web; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Net; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Logging; @@ -48,10 +49,10 @@ namespace Jellyfin.Api.Auth var claims = new[] { - new Claim(ClaimTypes.Name, user.Name), + new Claim(ClaimTypes.Name, user.Username), new Claim( ClaimTypes.Role, - value: user.Policy.IsAdministrator ? UserRoles.Administrator : UserRoles.User) + value: user.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User) }; var identity = new ClaimsIdentity(claims, Scheme.Name); var principal = new ClaimsPrincipal(identity); diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index afc9b8f3d..f965d83f3 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -98,7 +98,7 @@ namespace Jellyfin.Api.Controllers var user = _userManager.Users.First(); return new StartupUserDto { - Name = user.Name, + Name = user.Username, Password = user.Password }; } @@ -113,7 +113,7 @@ namespace Jellyfin.Api.Controllers { var user = _userManager.Users.First(); - user.Name = startupUserDto.Name; + user.Username = startupUserDto.Name; _userManager.UpdateUser(user); diff --git a/Jellyfin.Data/DayOfWeekHelper.cs b/Jellyfin.Data/DayOfWeekHelper.cs new file mode 100644 index 000000000..33410b732 --- /dev/null +++ b/Jellyfin.Data/DayOfWeekHelper.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using Jellyfin.Data.Enums; + +namespace Jellyfin.Data +{ + public static class DayOfWeekHelper + { + public static List GetDaysOfWeek(DynamicDayOfWeek day) + { + return GetDaysOfWeek(new List { day }); + } + + public static List GetDaysOfWeek(List days) + { + var list = new List(); + + if (days.Contains(DynamicDayOfWeek.Sunday) || + days.Contains(DynamicDayOfWeek.Weekend) || + days.Contains(DynamicDayOfWeek.Everyday)) + { + list.Add(DayOfWeek.Sunday); + } + + if (days.Contains(DynamicDayOfWeek.Saturday) || + days.Contains(DynamicDayOfWeek.Weekend) || + days.Contains(DynamicDayOfWeek.Everyday)) + { + list.Add(DayOfWeek.Saturday); + } + + if (days.Contains(DynamicDayOfWeek.Monday) || + days.Contains(DynamicDayOfWeek.Weekday) || + days.Contains(DynamicDayOfWeek.Everyday)) + { + list.Add(DayOfWeek.Monday); + } + + if (days.Contains(DynamicDayOfWeek.Tuesday) || + days.Contains(DynamicDayOfWeek.Weekday) || + days.Contains(DynamicDayOfWeek.Everyday)) + { + list.Add(DayOfWeek.Tuesday); + } + + if (days.Contains(DynamicDayOfWeek.Wednesday) || + days.Contains(DynamicDayOfWeek.Weekday) || + days.Contains(DynamicDayOfWeek.Everyday)) + { + list.Add(DayOfWeek.Wednesday); + } + + if (days.Contains(DynamicDayOfWeek.Thursday) || + days.Contains(DynamicDayOfWeek.Weekday) || + days.Contains(DynamicDayOfWeek.Everyday)) + { + list.Add(DayOfWeek.Thursday); + } + + if (days.Contains(DynamicDayOfWeek.Friday) || + days.Contains(DynamicDayOfWeek.Weekday) || + days.Contains(DynamicDayOfWeek.Everyday)) + { + list.Add(DayOfWeek.Friday); + } + + return list; + } + } +} diff --git a/Jellyfin.Data/Entities/AccessSchedule.cs b/Jellyfin.Data/Entities/AccessSchedule.cs new file mode 100644 index 000000000..7966cdb50 --- /dev/null +++ b/Jellyfin.Data/Entities/AccessSchedule.cs @@ -0,0 +1,68 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Enums; + +namespace Jellyfin.Data.Entities +{ + public class AccessSchedule + { + /// + /// Initializes a new instance of the class. + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected AccessSchedule() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The day of the week. + /// The start hour. + /// The end hour. + public AccessSchedule(DynamicDayOfWeek dayOfWeek, double startHour, double endHour) + { + DayOfWeek = dayOfWeek; + StartHour = startHour; + EndHour = endHour; + } + + /// + /// Factory method + /// + /// The day of the week. + /// The start hour. + /// The end hour. + /// The newly created instance. + public static AccessSchedule CreateInstance(DynamicDayOfWeek dayOfWeek, double startHour, double endHour) + { + return new AccessSchedule(dayOfWeek, startHour, endHour); + } + + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// + /// Gets or sets the day of week. + /// + /// The day of week. + [Required] + public DynamicDayOfWeek DayOfWeek { get; set; } + + /// + /// Gets or sets the start hour. + /// + /// The start hour. + [Required] + public double StartHour { get; set; } + + /// + /// Gets or sets the end hour. + /// + /// The end hour. + [Required] + public double EndHour { get; set; } + } +} diff --git a/Jellyfin.Data/Entities/ImageInfo.cs b/Jellyfin.Data/Entities/ImageInfo.cs new file mode 100644 index 000000000..336c13b36 --- /dev/null +++ b/Jellyfin.Data/Entities/ImageInfo.cs @@ -0,0 +1,25 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Jellyfin.Data.Entities +{ + public class ImageInfo + { + public ImageInfo(string path) + { + Path = path; + LastModified = DateTime.UtcNow; + } + + [Key] + [Required] + + public int Id { get; protected set; } + + [Required] + public string Path { get; set; } + + [Required] + public DateTime LastModified { get; set; } + } +} diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs index 715969dbf..1aaa8a180 100644 --- a/Jellyfin.Data/Entities/User.cs +++ b/Jellyfin.Data/Entities/User.cs @@ -1,18 +1,20 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using System.Globalization; using System.Linq; -using System.Runtime.CompilerServices; +using Jellyfin.Data.Enums; namespace Jellyfin.Data.Entities { [Table("User")] - public partial class User + public class User { - partial void Init(); + /// + /// The values being delimited here are Guids, so commas work as they do not appear in Guids. + /// + private const char Delimiter = ','; /// /// Default constructor. Protected due to required properties, but present because EF needs it. @@ -23,69 +25,86 @@ namespace Jellyfin.Data.Entities Permissions = new HashSet(); ProviderMappings = new HashSet(); Preferences = new HashSet(); - - Init(); + AccessSchedules = new HashSet(); } /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// Public constructor with required data /// - public static User CreateUserUnsafe() + /// + /// + /// + /// + /// + /// + public User( + string username, + bool mustUpdatePassword, + string authenticationProviderId, + int invalidLoginAttemptCount, + SubtitlePlaybackMode subtitleMode, + bool playDefaultAudioTrack) { - return new User(); + if (string.IsNullOrEmpty(username)) + { + throw new ArgumentNullException(nameof(username)); + } + + if (string.IsNullOrEmpty(authenticationProviderId)) + { + throw new ArgumentNullException(nameof(authenticationProviderId)); + } + + Username = username; + MustUpdatePassword = mustUpdatePassword; + AuthenticationProviderId = authenticationProviderId; + InvalidLoginAttemptCount = invalidLoginAttemptCount; + SubtitleMode = subtitleMode; + PlayDefaultAudioTrack = playDefaultAudioTrack; + + Groups = new HashSet(); + Permissions = new HashSet(); + ProviderMappings = new HashSet(); + Preferences = new HashSet(); + AccessSchedules = new HashSet(); + + // Set default values + Id = Guid.NewGuid(); + DisplayMissingEpisodes = false; + DisplayCollectionsView = false; + HidePlayedInLatest = true; + RememberAudioSelections = true; + RememberSubtitleSelections = true; + EnableNextEpisodeAutoPlay = true; + EnableAutoLogin = false; } /// - /// Public constructor with required data + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. /// - /// - /// - /// - /// - /// - /// - /// - public User(string username, bool mustupdatepassword, string audiolanguagepreference, string authenticationproviderid, int invalidloginattemptcount, string subtitlemode, bool playdefaultaudiotrack) + public static User CreateUserUnsafe() { - if (string.IsNullOrEmpty(username)) throw new ArgumentNullException(nameof(username)); - this.Username = username; - - this.MustUpdatePassword = mustupdatepassword; - - if (string.IsNullOrEmpty(audiolanguagepreference)) throw new ArgumentNullException(nameof(audiolanguagepreference)); - this.AudioLanguagePreference = audiolanguagepreference; - - if (string.IsNullOrEmpty(authenticationproviderid)) throw new ArgumentNullException(nameof(authenticationproviderid)); - this.AuthenticationProviderId = authenticationproviderid; - - this.InvalidLoginAttemptCount = invalidloginattemptcount; - - if (string.IsNullOrEmpty(subtitlemode)) throw new ArgumentNullException(nameof(subtitlemode)); - this.SubtitleMode = subtitlemode; - - this.PlayDefaultAudioTrack = playdefaultaudiotrack; - - this.Groups = new HashSet(); - this.Permissions = new HashSet(); - this.ProviderMappings = new HashSet(); - this.Preferences = new HashSet(); - - Init(); + return new User(); } /// /// Static create function (for use in LINQ queries, etc.) /// /// - /// - /// - /// - /// - /// - /// - public static User Create(string username, bool mustupdatepassword, string audiolanguagepreference, string authenticationproviderid, int invalidloginattemptcount, string subtitlemode, bool playdefaultaudiotrack) + /// + /// + /// + /// + /// + public static User Create( + string username, + bool mustUpdatePassword, + string authenticationProviderId, + int invalidLoginAttemptCount, + SubtitlePlaybackMode subtitleMode, + bool playDefaultAudioTrack) { - return new User(username, mustupdatepassword, audiolanguagepreference, authenticationproviderid, invalidloginattemptcount, subtitlemode, playdefaultaudiotrack); + return new User(username, mustUpdatePassword, authenticationProviderId, invalidLoginAttemptCount, subtitleMode, playDefaultAudioTrack); } /************************************************************************* @@ -97,8 +116,7 @@ namespace Jellyfin.Data.Entities /// [Key] [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public Guid Id { get; protected set; } /// /// Required, Max length = 255 @@ -115,6 +133,13 @@ namespace Jellyfin.Data.Entities [StringLength(65535)] public string Password { get; set; } + /// + /// Max length = 65535. + /// + [MaxLength(65535)] + [StringLength(65535)] + public string EasyPassword { get; set; } + /// /// Required /// @@ -122,9 +147,8 @@ namespace Jellyfin.Data.Entities public bool MustUpdatePassword { get; set; } /// - /// Required, Max length = 255 + /// Max length = 255. /// - [Required] [MaxLength(255)] [StringLength(255)] public string AudioLanguagePreference { get; set; } @@ -137,12 +161,10 @@ namespace Jellyfin.Data.Entities [StringLength(255)] public string AuthenticationProviderId { get; set; } - /// - /// Max length = 65535 - /// - [MaxLength(65535)] - [StringLength(65535)] - public string GroupedFolders { get; set; } + [Required] + [MaxLength(255)] + [StringLength(255)] + public string PasswordResetProviderId { get; set; } /// /// Required @@ -150,36 +172,17 @@ namespace Jellyfin.Data.Entities [Required] public int InvalidLoginAttemptCount { get; set; } - /// - /// Max length = 65535 - /// - [MaxLength(65535)] - [StringLength(65535)] - public string LatestItemExcludes { get; set; } + public DateTime LastActivityDate { get; set; } - public int? LoginAttemptsBeforeLockout { get; set; } + public DateTime LastLoginDate { get; set; } - /// - /// Max length = 65535 - /// - [MaxLength(65535)] - [StringLength(65535)] - public string MyMediaExcludes { get; set; } - - /// - /// Max length = 65535 - /// - [MaxLength(65535)] - [StringLength(65535)] - public string OrderedViews { get; set; } + public int? LoginAttemptsBeforeLockout { get; set; } /// - /// Required, Max length = 255 + /// Required. /// [Required] - [MaxLength(255)] - [StringLength(255)] - public string SubtitleMode { get; set; } + public SubtitlePlaybackMode SubtitleMode { get; set; } /// /// Required @@ -192,21 +195,47 @@ namespace Jellyfin.Data.Entities /// [MaxLength(255)] [StringLength(255)] - public string SubtitleLanguagePrefernce { get; set; } + public string SubtitleLanguagePreference { get; set; } + + [Required] + public bool DisplayMissingEpisodes { get; set; } + + [Required] + public bool DisplayCollectionsView { get; set; } + + [Required] + public bool EnableLocalPassword { get; set; } + + [Required] + public bool HidePlayedInLatest { get; set; } + + [Required] + public bool RememberAudioSelections { get; set; } - public bool? DisplayMissingEpisodes { get; set; } + [Required] + public bool RememberSubtitleSelections { get; set; } - public bool? DisplayCollectionsView { get; set; } + [Required] + public bool EnableNextEpisodeAutoPlay { get; set; } + + [Required] + public bool EnableAutoLogin { get; set; } - public bool? HidePlayedInLatest { get; set; } + [Required] + public bool EnableUserPreferenceAccess { get; set; } - public bool? RememberAudioSelections { get; set; } + public int? MaxParentalAgeRating { get; set; } - public bool? RememberSubtitleSelections { get; set; } + public int? RemoteClientBitrateLimit { get; set; } - public bool? EnableNextEpisodeAutoPlay { get; set; } + /// + /// This is a temporary stopgap for until the library db is migrated. + /// This corresponds to the value of the index of this user in the library db. + /// + [Required] + public long InternalId { get; set; } - public bool? EnableUserPreferenceAccess { get; set; } + public ImageInfo ProfileImage { get; set; } /// /// Required, ConcurrenyToken @@ -224,17 +253,76 @@ namespace Jellyfin.Data.Entities * Navigation properties *************************************************************************/ [ForeignKey("Group_Groups_Id")] - public virtual ICollection Groups { get; protected set; } + public ICollection Groups { get; protected set; } [ForeignKey("Permission_Permissions_Id")] - public virtual ICollection Permissions { get; protected set; } + public ICollection Permissions { get; protected set; } [ForeignKey("ProviderMapping_ProviderMappings_Id")] - public virtual ICollection ProviderMappings { get; protected set; } + public ICollection ProviderMappings { get; protected set; } [ForeignKey("Preference_Preferences_Id")] - public virtual ICollection Preferences { get; protected set; } + public ICollection Preferences { get; protected set; } + public ICollection AccessSchedules { get; protected set; } + + public bool HasPermission(PermissionKind permission) + { + return Permissions.Select(p => p.Kind).Contains(permission); + } + + public void SetPermission(PermissionKind kind, bool value) + { + var permissionObj = Permissions.First(p => p.Kind == kind); + permissionObj.Value = value; + } + + public string[] GetPreference(PreferenceKind preference) + { + return Preferences + .Where(p => p.Kind == preference) + .Select(p => p.Value) + .First() + .Split(Delimiter); + } + + public void SetPreference(PreferenceKind preference, string[] values) + { + var pref = Preferences.First(p => p.Kind == preference); + + pref.Value = string.Join(Delimiter.ToString(CultureInfo.InvariantCulture), values); + } + + public bool IsParentalScheduleAllowed() + { + var schedules = this.AccessSchedules; + + return schedules.Count == 0 || schedules.Any(i => IsParentalScheduleAllowed(i, DateTime.Now)); + } + + public bool IsFolderGrouped(Guid id) + { + return GetPreference(PreferenceKind.GroupedFolders).Any(i => new Guid(i) == id); + } + + private bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date) + { + if (date.Kind != DateTimeKind.Utc) + { + throw new ArgumentException("Utc date expected"); + } + + var localTime = date.ToLocalTime(); + + return DayOfWeekHelper.GetDaysOfWeek(schedule.DayOfWeek).Contains(localTime.DayOfWeek) && + IsWithinTime(schedule, localTime); + } + + private bool IsWithinTime(AccessSchedule schedule, DateTime localTime) + { + var hour = localTime.TimeOfDay.TotalHours; + + return hour >= schedule.StartHour && hour <= schedule.EndHour; + } } } - diff --git a/Jellyfin.Data/Enums/DynamicDayOfWeek.cs b/Jellyfin.Data/Enums/DynamicDayOfWeek.cs new file mode 100644 index 000000000..a33cd9d1c --- /dev/null +++ b/Jellyfin.Data/Enums/DynamicDayOfWeek.cs @@ -0,0 +1,18 @@ +#pragma warning disable CS1591 + +namespace Jellyfin.Data.Enums +{ + public enum DynamicDayOfWeek + { + Sunday = 0, + Monday = 1, + Tuesday = 2, + Wednesday = 3, + Thursday = 4, + Friday = 5, + Saturday = 6, + Everyday = 7, + Weekday = 8, + Weekend = 9 + } +} diff --git a/Jellyfin.Data/Enums/PermissionKind.cs b/Jellyfin.Data/Enums/PermissionKind.cs index 4447fdb77..df18261e6 100644 --- a/Jellyfin.Data/Enums/PermissionKind.cs +++ b/Jellyfin.Data/Enums/PermissionKind.cs @@ -1,14 +1,11 @@ -using System; - namespace Jellyfin.Data.Enums { - public enum PermissionKind : Int32 + public enum PermissionKind { IsAdministrator, IsHidden, IsDisabled, - BlockUnrateditems, - EnbleSharedDeviceControl, + EnableSharedDeviceControl, EnableRemoteAccess, EnableLiveTvManagement, EnableLiveTvAccess, @@ -23,6 +20,8 @@ namespace Jellyfin.Data.Enums EnableAllChannels, EnableAllFolders, EnablePublicSharing, - AccessSchedules + EnableRemoteControlOfOtherUsers, + EnablePlaybackRemuxing, + ForceRemoteSourceTranscoding } } diff --git a/Jellyfin.Data/Enums/PreferenceKind.cs b/Jellyfin.Data/Enums/PreferenceKind.cs index e66a51cae..34e20ead6 100644 --- a/Jellyfin.Data/Enums/PreferenceKind.cs +++ b/Jellyfin.Data/Enums/PreferenceKind.cs @@ -2,14 +2,19 @@ using System; namespace Jellyfin.Data.Enums { - public enum PreferenceKind : Int32 + public enum PreferenceKind { - MaxParentalRating, BlockedTags, - RemoteClientBitrateLimit, + BlockedChannels, + BlockedMediaFolders, EnabledDevices, EnabledChannels, EnabledFolders, - EnableContentDeletionFromFolders + EnableContentDeletionFromFolders, + LatestItemExcludes, + MyMediaExcludes, + GroupedFolders, + BlockUnratedItems, + OrderedViews } } diff --git a/Jellyfin.Data/Enums/SubtitlePlaybackMode.cs b/Jellyfin.Data/Enums/SubtitlePlaybackMode.cs new file mode 100644 index 000000000..c8fc21159 --- /dev/null +++ b/Jellyfin.Data/Enums/SubtitlePlaybackMode.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 + +namespace Jellyfin.Data.Enums +{ + public enum SubtitlePlaybackMode + { + Default = 0, + Always = 1, + OnlyForced = 2, + None = 3, + Smart = 4 + } +} diff --git a/Jellyfin.Data/Enums/UnratedItem.cs b/Jellyfin.Data/Enums/UnratedItem.cs new file mode 100644 index 000000000..5259e7739 --- /dev/null +++ b/Jellyfin.Data/Enums/UnratedItem.cs @@ -0,0 +1,17 @@ +#pragma warning disable CS1591 + +namespace Jellyfin.Data.Enums +{ + public enum UnratedItem + { + Movie, + Trailer, + Series, + Music, + Book, + LiveTvChannel, + LiveTvProgram, + ChannelContent, + Other + } +} diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index 8b6b7cacc..7e1314690 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -20,7 +20,7 @@ namespace Jellyfin.Server.Implementations public virtual DbSet Permissions { get; set; } public virtual DbSet Preferences { get; set; } public virtual DbSet ProviderMappings { get; set; } - public virtual DbSet Users { get; set; } + public virtual DbSet Users { get; set; } /*public virtual DbSet Artwork { get; set; } public virtual DbSet Books { get; set; } public virtual DbSet BookMetadata { get; set; } diff --git a/Jellyfin.Server.Implementations/User/DefaultAuthenticationProvider.cs b/Jellyfin.Server.Implementations/User/DefaultAuthenticationProvider.cs new file mode 100644 index 000000000..024500bf8 --- /dev/null +++ b/Jellyfin.Server.Implementations/User/DefaultAuthenticationProvider.cs @@ -0,0 +1,185 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Common; +using MediaBrowser.Common.Cryptography; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Model.Cryptography; + +namespace Jellyfin.Server.Implementations.User +{ + /// + /// The default authentication provider. + /// + public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser + { + private readonly ICryptoProvider _cryptographyProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The cryptography provider. + public DefaultAuthenticationProvider(ICryptoProvider cryptographyProvider) + { + _cryptographyProvider = cryptographyProvider; + } + + /// + public string Name => "Default"; + + /// + public bool IsEnabled => true; + + /// + // This is dumb and an artifact of the backwards way auth providers were designed. + // This version of authenticate was never meant to be called, but needs to be here for interface compat + // Only the providers that don't provide local user support use this + public Task Authenticate(string username, string password) + { + throw new NotImplementedException(); + } + + /// + // This is the version that we need to use for local users. Because reasons. + public Task Authenticate(string username, string password, Data.Entities.User resolvedUser) + { + if (resolvedUser == null) + { + throw new AuthenticationException("Specified user does not exist."); + } + + bool success = false; + + // As long as jellyfin supports passwordless users, we need this little block here to accommodate + if (!HasPassword(resolvedUser)) + { + return Task.FromResult(new ProviderAuthenticationResult + { + Username = username + }); + } + + byte[] passwordBytes = Encoding.UTF8.GetBytes(password); + + PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password); + if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id) + || _cryptographyProvider.DefaultHashMethod == readyHash.Id) + { + byte[] calculatedHash = _cryptographyProvider.ComputeHash( + readyHash.Id, + passwordBytes, + readyHash.Salt.ToArray()); + + if (readyHash.Hash.SequenceEqual(calculatedHash)) + { + success = true; + } + } + else + { + throw new AuthenticationException($"Requested crypto method not available in provider: {readyHash.Id}"); + } + + if (!success) + { + throw new AuthenticationException("Invalid username or password"); + } + + return Task.FromResult(new ProviderAuthenticationResult + { + Username = username + }); + } + + /// + public bool HasPassword(Data.Entities.User user) + => !string.IsNullOrEmpty(user.Password); + + /// + public Task ChangePassword(Data.Entities.User user, string newPassword) + { + if (string.IsNullOrEmpty(newPassword)) + { + user.Password = null; + return Task.CompletedTask; + } + + PasswordHash newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword); + user.Password = newPasswordHash.ToString(); + + return Task.CompletedTask; + } + + /// + public void ChangeEasyPassword(Data.Entities.User user, string newPassword, string newPasswordHash) + { + if (newPassword != null) + { + newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword).ToString(); + } + + if (string.IsNullOrWhiteSpace(newPasswordHash)) + { + throw new ArgumentNullException(nameof(newPasswordHash)); + } + + user.EasyPassword = newPasswordHash; + } + + /// + public string GetEasyPasswordHash(Data.Entities.User user) + { + return string.IsNullOrEmpty(user.EasyPassword) + ? null + : Hex.Encode(PasswordHash.Parse(user.EasyPassword).Hash); + } + + /// + /// Hashes the provided string. + /// + /// The user. + /// The string to hash. + /// The hashed string. + public string GetHashedString(Data.Entities.User user, string str) + { + if (string.IsNullOrEmpty(user.Password)) + { + return _cryptographyProvider.CreatePasswordHash(str).ToString(); + } + + // TODO: make use of iterations parameter? + PasswordHash passwordHash = PasswordHash.Parse(user.Password); + var salt = passwordHash.Salt.ToArray(); + return new PasswordHash( + passwordHash.Id, + _cryptographyProvider.ComputeHash( + passwordHash.Id, + Encoding.UTF8.GetBytes(str), + salt), + salt, + passwordHash.Parameters.ToDictionary(x => x.Key, y => y.Value)).ToString(); + } + + /// + /// Hashes the provided string. + /// + /// The user. + /// The string to hash. + /// The hashed string. + public ReadOnlySpan GetHashed(Data.Entities.User user, string str) + { + if (string.IsNullOrEmpty(user.Password)) + { + return _cryptographyProvider.CreatePasswordHash(str).Hash; + } + + // TODO: make use of iterations parameter? + PasswordHash passwordHash = PasswordHash.Parse(user.Password); + return _cryptographyProvider.ComputeHash( + passwordHash.Id, + Encoding.UTF8.GetBytes(str), + passwordHash.Salt.ToArray()); + } + } +} diff --git a/Jellyfin.Server.Implementations/User/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/User/DefaultPasswordResetProvider.cs new file mode 100644 index 000000000..80ab3ce00 --- /dev/null +++ b/Jellyfin.Server.Implementations/User/DefaultPasswordResetProvider.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Users; + +namespace Jellyfin.Server.Implementations.User +{ + /// + /// The default password reset provider. + /// + public class DefaultPasswordResetProvider : IPasswordResetProvider + { + private const string BaseResetFileName = "passwordreset"; + + private readonly IJsonSerializer _jsonSerializer; + private readonly IUserManager _userManager; + + private readonly string _passwordResetFileBase; + private readonly string _passwordResetFileBaseDir; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration manager. + /// The JSON serializer. + /// The user manager. + public DefaultPasswordResetProvider( + IServerConfigurationManager configurationManager, + IJsonSerializer jsonSerializer, + IUserManager userManager) + { + _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath; + _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName); + _jsonSerializer = jsonSerializer; + _userManager = userManager; + } + + /// + public string Name => "Default Password Reset Provider"; + + /// + public bool IsEnabled => true; + + /// + public async Task RedeemPasswordResetPin(string pin) + { + SerializablePasswordReset spr; + List usersReset = new List(); + foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*")) + { + await using (var str = File.OpenRead(resetFile)) + { + spr = await _jsonSerializer.DeserializeFromStreamAsync(str).ConfigureAwait(false); + } + + if (spr.ExpirationDate < DateTime.Now) + { + File.Delete(resetFile); + } + else if (string.Equals( + spr.Pin.Replace("-", string.Empty, StringComparison.Ordinal), + pin.Replace("-", string.Empty, StringComparison.Ordinal), + StringComparison.InvariantCultureIgnoreCase)) + { + var resetUser = _userManager.GetUserByName(spr.UserName); + if (resetUser == null) + { + throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found"); + } + + await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); + usersReset.Add(resetUser.Username); + File.Delete(resetFile); + } + } + + if (usersReset.Count < 1) + { + throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}"); + } + + return new PinRedeemResult + { + Success = true, + UsersReset = usersReset.ToArray() + }; + } + + /// + public async Task StartForgotPasswordProcess(Jellyfin.Data.Entities.User user, bool isInNetwork) + { + string pin; + using (var cryptoRandom = RandomNumberGenerator.Create()) + { + byte[] bytes = new byte[4]; + cryptoRandom.GetBytes(bytes); + pin = BitConverter.ToString(bytes); + } + + DateTime expireTime = DateTime.Now.AddMinutes(30); + + user.EasyPassword = pin; + await _userManager.UpdateUserAsync(user).ConfigureAwait(false); + + return new ForgotPasswordResult + { + Action = ForgotPasswordAction.PinCode, + PinExpirationDate = expireTime, + }; + } + + private class SerializablePasswordReset : PasswordPinCreationResult + { + public string Pin { get; set; } + + public string UserName { get; set; } + } + } +} diff --git a/Jellyfin.Server.Implementations/User/DeviceAccessEntryPoint.cs b/Jellyfin.Server.Implementations/User/DeviceAccessEntryPoint.cs new file mode 100644 index 000000000..d33034ab2 --- /dev/null +++ b/Jellyfin.Server.Implementations/User/DeviceAccessEntryPoint.cs @@ -0,0 +1,65 @@ +#pragma warning disable CS1591 + +using System.Threading.Tasks; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Security; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Events; + +namespace Jellyfin.Server.Implementations.User +{ + public sealed class DeviceAccessEntryPoint : IServerEntryPoint + { + private readonly IUserManager _userManager; + private readonly IAuthenticationRepository _authRepo; + private readonly IDeviceManager _deviceManager; + private readonly ISessionManager _sessionManager; + + public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager) + { + _userManager = userManager; + _authRepo = authRepo; + _deviceManager = deviceManager; + _sessionManager = sessionManager; + } + + public Task RunAsync() + { + _userManager.OnUserUpdated += OnUserUpdated; + + return Task.CompletedTask; + } + + private void OnUserUpdated(object sender, GenericEventArgs e) + { + var user = e.Argument; + if (!user.HasPermission(PermissionKind.EnableAllDevices)) + { + UpdateDeviceAccess(user); + } + } + + public void Dispose() + { + } + + private void UpdateDeviceAccess(Data.Entities.User user) + { + var existing = _authRepo.Get(new AuthenticationInfoQuery + { + UserId = user.Id + }).Items; + + foreach (var authInfo in existing) + { + if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId)) + { + _sessionManager.Logout(authInfo); + } + } + } + } +} diff --git a/Jellyfin.Server.Implementations/User/InvalidAuthProvider.cs b/Jellyfin.Server.Implementations/User/InvalidAuthProvider.cs new file mode 100644 index 000000000..a11ca128a --- /dev/null +++ b/Jellyfin.Server.Implementations/User/InvalidAuthProvider.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using MediaBrowser.Controller.Authentication; + +namespace Jellyfin.Server.Implementations.User +{ + /// + /// An invalid authentication provider. + /// + public class InvalidAuthProvider : IAuthenticationProvider + { + /// + public string Name => "InvalidOrMissingAuthenticationProvider"; + + /// + public bool IsEnabled => true; + + /// + public Task Authenticate(string username, string password) + { + throw new AuthenticationException("User Account cannot login with this provider. The Normal provider for this user cannot be found"); + } + + /// + public bool HasPassword(Data.Entities.User user) + { + return true; + } + + /// + public Task ChangePassword(Data.Entities.User user, string newPassword) + { + return Task.CompletedTask; + } + + /// + public void ChangeEasyPassword(Data.Entities.User user, string newPassword, string newPasswordHash) + { + // Nothing here + } + + /// + public string GetEasyPasswordHash(Data.Entities.User user) + { + return string.Empty; + } + } +} diff --git a/Jellyfin.Server.Implementations/User/UserManager.cs b/Jellyfin.Server.Implementations/User/UserManager.cs new file mode 100644 index 000000000..1ed11cfcb --- /dev/null +++ b/Jellyfin.Server.Implementations/User/UserManager.cs @@ -0,0 +1,749 @@ +#pragma warning disable CS0067 +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Jellyfin.Data.Enums; +using MediaBrowser.Common.Cryptography; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.Users; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Implementations.User +{ + public class UserManager : IUserManager + { + private readonly JellyfinDbProvider _dbProvider; + private readonly ICryptoProvider _cryptoProvider; + private readonly INetworkManager _networkManager; + private readonly ILogger _logger; + + private IAuthenticationProvider[] _authenticationProviders; + private DefaultAuthenticationProvider _defaultAuthenticationProvider; + private InvalidAuthProvider _invalidAuthProvider; + private IPasswordResetProvider[] _passwordResetProviders; + private DefaultPasswordResetProvider _defaultPasswordResetProvider; + + public UserManager( + JellyfinDbProvider dbProvider, + ICryptoProvider cryptoProvider, + INetworkManager networkManager, + ILogger logger) + { + _dbProvider = dbProvider; + _cryptoProvider = cryptoProvider; + _networkManager = networkManager; + _logger = logger; + } + + public event EventHandler> OnUserPasswordChanged; + + /// + public event EventHandler> OnUserUpdated; + + /// + public event EventHandler> OnUserCreated; + + /// + public event EventHandler> OnUserDeleted; + + public event EventHandler> OnUserLockedOut; + + public IEnumerable Users + { + get + { + using var dbContext = _dbProvider.CreateContext(); + return dbContext.Users; + } + } + + public IEnumerable UsersIds + { + get + { + using var dbContext = _dbProvider.CreateContext(); + return dbContext.Users.Select(u => u.Id); + } + } + + public Data.Entities.User GetUserById(Guid id) + { + if (id == Guid.Empty) + { + throw new ArgumentException("Guid can't be empty", nameof(id)); + } + + using var dbContext = _dbProvider.CreateContext(); + + return dbContext.Users.Find(id); + } + + public Data.Entities.User GetUserByName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Invalid username", nameof(name)); + } + + using var dbContext = _dbProvider.CreateContext(); + + return dbContext.Users.FirstOrDefault(u => + string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase)); + } + + public async Task RenameUser(Data.Entities.User user, string newName) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (string.IsNullOrWhiteSpace(newName)) + { + throw new ArgumentException("Invalid username", nameof(newName)); + } + + if (user.Username.Equals(newName, StringComparison.Ordinal)) + { + throw new ArgumentException("The new and old names must be different."); + } + + if (Users.Any( + u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException(string.Format( + CultureInfo.InvariantCulture, + "A user with the name '{0}' already exists.", + newName)); + } + + user.Username = newName; + await UpdateUserAsync(user).ConfigureAwait(false); + + OnUserUpdated?.Invoke(this, new GenericEventArgs(user)); + } + + public void UpdateUser(Data.Entities.User user) + { + using var dbContext = _dbProvider.CreateContext(); + dbContext.Users.Update(user); + dbContext.SaveChanges(); + } + + public async Task UpdateUserAsync(Data.Entities.User user) + { + await using var dbContext = _dbProvider.CreateContext(); + dbContext.Users.Update(user); + + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + + public Data.Entities.User CreateUser(string name) + { + using var dbContext = _dbProvider.CreateContext(); + + var newUser = CreateUserObject(name); + dbContext.Users.Add(newUser); + dbContext.SaveChanges(); + + return newUser; + } + + public void DeleteUser(Data.Entities.User user) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + using var dbContext = _dbProvider.CreateContext(); + + if (!dbContext.Users.Contains(user)) + { + throw new ArgumentException(string.Format( + CultureInfo.InvariantCulture, + "The user cannot be deleted because there is no user with the Name {0} and Id {1}.", + user.Username, + user.Id)); + } + + if (dbContext.Users.Count() == 1) + { + throw new InvalidOperationException(string.Format( + CultureInfo.InvariantCulture, + "The user '{0}' cannot be deleted because there must be at least one user in the system.", + user.Username)); + } + + if (user.HasPermission(PermissionKind.IsAdministrator) + && Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1) + { + throw new ArgumentException( + string.Format( + CultureInfo.InvariantCulture, + "The user '{0}' cannot be deleted because there must be at least one admin user in the system.", + user.Username), + nameof(user)); + } + + dbContext.Users.Remove(user); + dbContext.SaveChanges(); + } + + public Task ResetPassword(Data.Entities.User user) + { + return ChangePassword(user, string.Empty); + } + + public void ResetEasyPassword(Data.Entities.User user) + { + ChangeEasyPassword(user, string.Empty, null); + } + + public async Task ChangePassword(Data.Entities.User user, string newPassword) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false); + await UpdateUserAsync(user).ConfigureAwait(false); + + OnUserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); + } + + public void ChangeEasyPassword(Data.Entities.User user, string newPassword, string newPasswordSha1) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + GetAuthenticationProvider(user).ChangeEasyPassword(user, newPassword, newPasswordSha1); + + UpdateUser(user); + + OnUserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); + } + + public UserDto GetUserDto(Data.Entities.User user, string remoteEndPoint = null) + { + return new UserDto + { + Id = user.Id, + HasPassword = user.Password == null, + EnableAutoLogin = user.EnableAutoLogin, + LastLoginDate = user.LastLoginDate, + LastActivityDate = user.LastActivityDate, + Configuration = new UserConfiguration + { + SubtitleMode = user.SubtitleMode, + HidePlayedInLatest = user.HidePlayedInLatest, + EnableLocalPassword = user.EnableLocalPassword, + PlayDefaultAudioTrack = user.PlayDefaultAudioTrack, + DisplayCollectionsView = user.DisplayCollectionsView, + DisplayMissingEpisodes = user.DisplayMissingEpisodes, + AudioLanguagePreference = user.AudioLanguagePreference, + RememberAudioSelections = user.RememberAudioSelections, + EnableNextEpisodeAutoPlay = user.EnableNextEpisodeAutoPlay, + RememberSubtitleSelections = user.RememberSubtitleSelections, + SubtitleLanguagePreference = user.SubtitleLanguagePreference, + OrderedViews = user.GetPreference(PreferenceKind.OrderedViews), + GroupedFolders = user.GetPreference(PreferenceKind.GroupedFolders), + MyMediaExcludes = user.GetPreference(PreferenceKind.MyMediaExcludes), + LatestItemsExcludes = user.GetPreference(PreferenceKind.LatestItemExcludes) + }, + Policy = new UserPolicy + { + MaxParentalRating = user.MaxParentalAgeRating, + EnableUserPreferenceAccess = user.EnableUserPreferenceAccess, + RemoteClientBitrateLimit = user.RemoteClientBitrateLimit.GetValueOrDefault(), + AuthenticatioIsnProviderId = user.AuthenticationProviderId, + PasswordResetProviderId = user.PasswordResetProviderId, + InvalidLoginAttemptCount = user.InvalidLoginAttemptCount, + LoginAttemptsBeforeLockout = user.LoginAttemptsBeforeLockout.GetValueOrDefault(), + IsAdministrator = user.HasPermission(PermissionKind.IsAdministrator), + IsHidden = user.HasPermission(PermissionKind.IsHidden), + IsDisabled = user.HasPermission(PermissionKind.IsDisabled), + EnableSharedDeviceControl = user.HasPermission(PermissionKind.EnableSharedDeviceControl), + EnableRemoteAccess = user.HasPermission(PermissionKind.EnableRemoteAccess), + EnableLiveTvManagement = user.HasPermission(PermissionKind.EnableLiveTvManagement), + EnableLiveTvAccess = user.HasPermission(PermissionKind.EnableLiveTvAccess), + EnableMediaPlayback = user.HasPermission(PermissionKind.EnableMediaPlayback), + EnableAudioPlaybackTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding), + EnableVideoPlaybackTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding), + EnableContentDeletion = user.HasPermission(PermissionKind.EnableContentDeletion), + EnableContentDownloading = user.HasPermission(PermissionKind.EnableContentDownloading), + EnableSyncTranscoding = user.HasPermission(PermissionKind.EnableSyncTranscoding), + EnableMediaConversion = user.HasPermission(PermissionKind.EnableMediaConversion), + EnableAllChannels = user.HasPermission(PermissionKind.EnableAllChannels), + EnableAllDevices = user.HasPermission(PermissionKind.EnableAllDevices), + EnableAllFolders = user.HasPermission(PermissionKind.EnableAllFolders), + EnableRemoteControlOfOtherUsers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers), + EnablePlaybackRemuxing = user.HasPermission(PermissionKind.EnablePlaybackRemuxing), + ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding), + EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing), + AccessSchedules = user.AccessSchedules.ToArray(), + BlockedTags = user.GetPreference(PreferenceKind.BlockedTags), + EnabledChannels = user.GetPreference(PreferenceKind.EnabledChannels), + EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices), + EnabledFolders = user.GetPreference(PreferenceKind.EnabledFolders), + EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders) + } + }; + } + + public async Task AuthenticateUser( + string username, + string password, + string passwordSha1, + string remoteEndPoint, + bool isUserSession) + { + if (string.IsNullOrWhiteSpace(username)) + { + _logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndPoint); + throw new ArgumentNullException(nameof(username)); + } + + var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); + bool success; + IAuthenticationProvider authenticationProvider; + + if (user != null) + { + var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint) + .ConfigureAwait(false); + authenticationProvider = authResult.authenticationProvider; + success = authResult.success; + } + else + { + var authResult = await AuthenticateLocalUser(username, password, null, remoteEndPoint) + .ConfigureAwait(false); + authenticationProvider = authResult.authenticationProvider; + string updatedUsername = authResult.username; + success = authResult.success; + + if (success + && authenticationProvider != null + && !(authenticationProvider is DefaultAuthenticationProvider)) + { + // Trust the username returned by the authentication provider + username = updatedUsername; + + // Search the database for the user again + // the authentication provider might have created it + user = Users + .FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); + + if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy) + { + UpdatePolicy(user.Id, hasNewUserPolicy.GetNewUserPolicy()); + + await UpdateUserAsync(user).ConfigureAwait(false); + } + } + } + + if (success && user != null && authenticationProvider != null) + { + var providerId = authenticationProvider.GetType().FullName; + + if (!string.Equals(providerId, user.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase)) + { + user.AuthenticationProviderId = providerId; + await UpdateUserAsync(user).ConfigureAwait(false); + } + } + + if (user == null) + { + _logger.LogInformation( + "Authentication request for {UserName} has been denied (IP: {IP}).", + username, + remoteEndPoint); + throw new AuthenticationException("Invalid username or password entered."); + } + + if (user.HasPermission(PermissionKind.IsDisabled)) + { + _logger.LogInformation( + "Authentication request for {UserName} has been denied because this account is currently disabled (IP: {IP}).", + username, + remoteEndPoint); + throw new SecurityException( + $"The {user.Username} account is currently disabled. Please consult with your administrator."); + } + + if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && + !_networkManager.IsInLocalNetwork(remoteEndPoint)) + { + _logger.LogInformation( + "Authentication request for {UserName} forbidden: remote access disabled and user not in local network (IP: {IP}).", + username, + remoteEndPoint); + throw new SecurityException("Forbidden."); + } + + if (!user.IsParentalScheduleAllowed()) + { + _logger.LogInformation( + "Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {IP}).", + username, + remoteEndPoint); + throw new SecurityException("User is not allowed access at this time."); + } + + // Update LastActivityDate and LastLoginDate, then save + if (success) + { + if (isUserSession) + { + user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; + UpdateUser(user); + } + + ResetInvalidLoginAttemptCount(user); + _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Username); + } + else + { + IncrementInvalidLoginAttemptCount(user); + _logger.LogInformation( + "Authentication request for {UserName} has been denied (IP: {IP}).", + user.Username, + remoteEndPoint); + } + + return success ? user : null; + } + + public async Task StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) + { + var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername); + + var action = ForgotPasswordAction.InNetworkRequired; + + if (user != null && isInNetwork) + { + var passwordResetProvider = GetPasswordResetProvider(user); + return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false); + } + + return new ForgotPasswordResult + { + Action = action, + PinFile = string.Empty + }; + } + + public async Task RedeemPasswordResetPin(string pin) + { + foreach (var provider in _passwordResetProviders) + { + var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false); + + if (result.Success) + { + return result; + } + } + + return new PinRedeemResult + { + Success = false, + UsersReset = Array.Empty() + }; + } + + public void AddParts(IEnumerable authenticationProviders, IEnumerable passwordResetProviders) + { + _authenticationProviders = authenticationProviders.ToArray(); + + _defaultAuthenticationProvider = _authenticationProviders.OfType().First(); + + _invalidAuthProvider = _authenticationProviders.OfType().First(); + + _passwordResetProviders = passwordResetProviders.ToArray(); + + _defaultPasswordResetProvider = passwordResetProviders.OfType().First(); + } + + public NameIdPair[] GetAuthenticationProviders() + { + return _authenticationProviders + .Where(provider => provider.IsEnabled) + .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1) + .ThenBy(i => i.Name) + .Select(i => new NameIdPair + { + Name = i.Name, + Id = i.GetType().FullName + }) + .ToArray(); + } + + public NameIdPair[] GetPasswordResetProviders() + { + return _passwordResetProviders + .Where(provider => provider.IsEnabled) + .OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1) + .ThenBy(i => i.Name) + .Select(i => new NameIdPair + { + Name = i.Name, + Id = i.GetType().FullName + }) + .ToArray(); + } + + public void UpdateConfiguration(Guid userId, UserConfiguration config) + { + var user = GetUserById(userId); + user.SubtitleMode = config.SubtitleMode; + user.HidePlayedInLatest = config.HidePlayedInLatest; + user.EnableLocalPassword = config.EnableLocalPassword; + user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack; + user.DisplayCollectionsView = config.DisplayCollectionsView; + user.DisplayMissingEpisodes = config.DisplayMissingEpisodes; + user.AudioLanguagePreference = config.AudioLanguagePreference; + user.RememberAudioSelections = config.RememberAudioSelections; + user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay; + user.RememberSubtitleSelections = config.RememberSubtitleSelections; + user.SubtitleLanguagePreference = config.SubtitleLanguagePreference; + + user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews); + user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders); + user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes); + user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes); + + UpdateUser(user); + } + + public void UpdatePolicy(Guid userId, UserPolicy policy) + { + var user = GetUserById(userId); + + user.MaxParentalAgeRating = policy.MaxParentalRating; + user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess; + user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit; + user.AuthenticationProviderId = policy.AuthenticatioIsnProviderId; + user.PasswordResetProviderId = policy.PasswordResetProviderId; + user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount; + user.LoginAttemptsBeforeLockout = policy.LoginAttemptsBeforeLockout == -1 + ? null + : new int?(policy.LoginAttemptsBeforeLockout); + user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator); + user.SetPermission(PermissionKind.IsHidden, policy.IsHidden); + user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled); + user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl); + user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess); + user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement); + user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess); + user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback); + user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding); + user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding); + user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion); + user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading); + user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding); + user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion); + user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels); + user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices); + user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders); + user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers); + user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing); + user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding); + user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing); + + user.AccessSchedules.Clear(); + foreach (var policyAccessSchedule in policy.AccessSchedules) + { + user.AccessSchedules.Add(policyAccessSchedule); + } + + user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags); + user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels); + user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices); + user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders); + user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders); + } + + private Data.Entities.User CreateUserObject(string name) + { + return new Data.Entities.User( + username: name, + mustUpdatePassword: false, + authenticationProviderId: _defaultAuthenticationProvider.GetType().FullName, + invalidLoginAttemptCount: -1, + subtitleMode: SubtitlePlaybackMode.Default, + playDefaultAudioTrack: true); + } + + private IAuthenticationProvider GetAuthenticationProvider(Data.Entities.User user) + { + return GetAuthenticationProviders(user)[0]; + } + + private IPasswordResetProvider GetPasswordResetProvider(Data.Entities.User user) + { + return GetPasswordResetProviders(user)[0]; + } + + private IList GetAuthenticationProviders(Data.Entities.User user) + { + var authenticationProviderId = user?.AuthenticationProviderId; + + var providers = _authenticationProviders.Where(i => i.IsEnabled).ToList(); + + if (!string.IsNullOrEmpty(authenticationProviderId)) + { + providers = providers.Where(i => string.Equals(authenticationProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase)).ToList(); + } + + if (providers.Count == 0) + { + // Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found + _logger.LogWarning( + "User {UserName} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. Assigning user to InvalidAuthProvider until this is corrected", + user?.Username, + user?.AuthenticationProviderId); + providers = new List + { + _invalidAuthProvider + }; + } + + return providers; + } + + private IList GetPasswordResetProviders(Data.Entities.User user) + { + var passwordResetProviderId = user?.PasswordResetProviderId; + var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray(); + + if (!string.IsNullOrEmpty(passwordResetProviderId)) + { + providers = providers.Where(i => + string.Equals(passwordResetProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + } + + if (providers.Length == 0) + { + providers = new IPasswordResetProvider[] + { + _defaultPasswordResetProvider + }; + } + + return providers; + } + + private async Task<(IAuthenticationProvider authenticationProvider, string username, bool success)> + AuthenticateLocalUser( + string username, + string password, + Jellyfin.Data.Entities.User user, + string remoteEndPoint) + { + bool success = false; + IAuthenticationProvider authenticationProvider = null; + + foreach (var provider in GetAuthenticationProviders(user)) + { + var providerAuthResult = + await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); + var updatedUsername = providerAuthResult.username; + success = providerAuthResult.success; + + if (success) + { + authenticationProvider = provider; + username = updatedUsername; + break; + } + } + + if (!success + && _networkManager.IsInLocalNetwork(remoteEndPoint) + && user?.EnableLocalPassword == true + && !string.IsNullOrEmpty(user.EasyPassword)) + { + // Check easy password + var passwordHash = PasswordHash.Parse(user.EasyPassword); + var hash = _cryptoProvider.ComputeHash( + passwordHash.Id, + Encoding.UTF8.GetBytes(password), + passwordHash.Salt.ToArray()); + success = passwordHash.Hash.SequenceEqual(hash); + } + + return (authenticationProvider, username, success); + } + + private async Task<(string username, bool success)> AuthenticateWithProvider( + IAuthenticationProvider provider, + string username, + string password, + Data.Entities.User resolvedUser) + { + try + { + var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser + ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false) + : await provider.Authenticate(username, password).ConfigureAwait(false); + + if (authenticationResult.Username != username) + { + _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username); + username = authenticationResult.Username; + } + + return (username, true); + } + catch (AuthenticationException ex) + { + _logger.LogError(ex, "Error authenticating with provider {Provider}", provider.Name); + + return (username, false); + } + } + + private void IncrementInvalidLoginAttemptCount(Data.Entities.User user) + { + int invalidLogins = user.InvalidLoginAttemptCount; + int? maxInvalidLogins = user.LoginAttemptsBeforeLockout; + if (maxInvalidLogins.HasValue + && invalidLogins >= maxInvalidLogins) + { + user.SetPermission(PermissionKind.IsDisabled, true); + OnUserLockedOut?.Invoke(this, new GenericEventArgs(user)); + _logger.LogWarning( + "Disabling user {UserName} due to {Attempts} unsuccessful login attempts.", + user.Username, + invalidLogins); + } + + UpdateUser(user); + } + + private void ResetInvalidLoginAttemptCount(Data.Entities.User user) + { + user.InvalidLoginAttemptCount = 0; + } + } +} diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs new file mode 100644 index 000000000..23d2299cd --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -0,0 +1,8 @@ +#pragma warning disable CS1591 + +namespace Jellyfin.Server.Migrations.Routines +{ + public class MigrateUserDb + { + } +} diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs index 1a1d86362..a5b504dfa 100644 --- a/MediaBrowser.Api/BaseApiService.cs +++ b/MediaBrowser.Api/BaseApiService.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -94,8 +95,8 @@ namespace MediaBrowser.Api var authenticatedUser = auth.User; // If they're going to update the record of another user, they must be an administrator - if ((!userId.Equals(auth.UserId) && !authenticatedUser.Policy.IsAdministrator) - || (restrictUserPreferences && !authenticatedUser.Policy.EnableUserPreferenceAccess)) + if ((!userId.Equals(auth.UserId) && !authenticatedUser.HasPermission(PermissionKind.IsAdministrator)) + || (restrictUserPreferences && !authenticatedUser.EnableUserPreferenceAccess)) { throw new SecurityException("Unauthorized access."); } diff --git a/MediaBrowser.Api/FilterService.cs b/MediaBrowser.Api/FilterService.cs index 5eb72cdb1..ac61cd491 100644 --- a/MediaBrowser.Api/FilterService.cs +++ b/MediaBrowser.Api/FilterService.cs @@ -220,7 +220,7 @@ namespace MediaBrowser.Api return result; } - private InternalItemsQuery GetItemsQuery(GetQueryFiltersLegacy request, User user) + private InternalItemsQuery GetItemsQuery(GetQueryFiltersLegacy request, Jellyfin.Data.Entities.User user) { var query = new InternalItemsQuery { diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index 2e9b3e6cb..f9976122c 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -20,6 +20,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; +using User = Jellyfin.Data.Entities.User; namespace MediaBrowser.Api.Images { @@ -399,14 +400,14 @@ namespace MediaBrowser.Api.Images { var item = _userManager.GetUserById(request.Id); - return GetImage(request, Guid.Empty, item, false); + return GetImage(request, item, false); } public object Head(GetUserImage request) { var item = _userManager.GetUserById(request.Id); - return GetImage(request, Guid.Empty, item, true); + return GetImage(request, item, true); } public object Get(GetItemByNameImage request) @@ -439,9 +440,9 @@ namespace MediaBrowser.Api.Images request.Type = Enum.Parse(GetPathValue(3).ToString(), true); - var item = _userManager.GetUserById(id); + var user = _userManager.GetUserById(id); - return PostImage(item, request.RequestStream, request.Type, Request.ContentType); + return PostImage(user, request.RequestStream, Request.ContentType); } /// @@ -468,9 +469,9 @@ namespace MediaBrowser.Api.Images var userId = request.Id; AssertCanUpdateUser(_authContext, _userManager, userId, true); - var item = _userManager.GetUserById(userId); + var user = _userManager.GetUserById(userId); - item.DeleteImage(request.Type, request.Index ?? 0); + user.ProfileImage = null; } /// @@ -555,18 +556,17 @@ namespace MediaBrowser.Api.Images var imageInfo = GetImageInfo(request, item); if (imageInfo == null) { - var displayText = item == null ? itemId.ToString() : item.Name; - throw new ResourceNotFoundException(string.Format("{0} does not have an image of type {1}", displayText, request.Type)); + throw new ResourceNotFoundException(string.Format("{0} does not have an image of type {1}", item.Name, request.Type)); } - bool cropwhitespace; + bool cropWhitespace; if (request.CropWhitespace.HasValue) { - cropwhitespace = request.CropWhitespace.Value; + cropWhitespace = request.CropWhitespace.Value; } else { - cropwhitespace = request.Type == ImageType.Logo || request.Type == ImageType.Art; + cropWhitespace = request.Type == ImageType.Logo || request.Type == ImageType.Art; } var outputFormats = GetOutputFormats(request); @@ -589,13 +589,94 @@ namespace MediaBrowser.Api.Images itemId, request, imageInfo, - cropwhitespace, + cropWhitespace, outputFormats, cacheDuration, responseHeaders, isHeadRequest); } + public Task GetImage(ImageRequest request, User user, bool isHeadRequest) + { + var imageInfo = GetImageInfo(request, user); + + TimeSpan? cacheDuration = null; + + if (!string.IsNullOrEmpty(request.Tag)) + { + cacheDuration = TimeSpan.FromDays(365); + } + + var responseHeaders = new Dictionary + { + {"transferMode.dlna.org", "Interactive"}, + {"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"} + }; + + var outputFormats = GetOutputFormats(request); + + return GetImageResult( + user, + user.Id, + request, + imageInfo, + false, + outputFormats, + cacheDuration, + responseHeaders, + isHeadRequest); + } + + private async Task GetImageResult( + User user, + Guid itemId, + ImageRequest request, + ItemImageInfo info, + bool cropWhitespace, + IReadOnlyCollection supportedFormats, + TimeSpan? cacheDuration, + IDictionary headers, + bool isHeadRequest) + { + var options = new ImageProcessingOptions + { + CropWhiteSpace = true, + Height = request.Height, + ImageIndex = request.Index ?? 0, + Image = info, + Item = null, // Hack alert + ItemId = itemId, + MaxHeight = request.MaxHeight, + MaxWidth = request.MaxWidth, + Quality = request.Quality ?? 100, + Width = request.Width, + AddPlayedIndicator = request.AddPlayedIndicator, + PercentPlayed = 0, + UnplayedCount = request.UnplayedCount, + Blur = request.Blur, + BackgroundColor = request.BackgroundColor, + ForegroundLayer = request.ForegroundLayer, + SupportedOutputFormats = supportedFormats + }; + + var imageResult = await _imageProcessor.ProcessImage(options).ConfigureAwait(false); + + headers[HeaderNames.Vary] = HeaderNames.Accept; + + return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions + { + CacheDuration = cacheDuration, + ResponseHeaders = headers, + ContentType = imageResult.Item2, + DateLastModified = imageResult.Item3, + IsHeadRequest = isHeadRequest, + Path = imageResult.Item1, + + FileShare = FileShare.Read + + }).ConfigureAwait(false); + } + private async Task GetImageResult( BaseItem item, Guid itemId, @@ -740,6 +821,28 @@ namespace MediaBrowser.Api.Images return item.GetImageInfo(request.Type, index); } + private ItemImageInfo GetImageInfo(ImageRequest request, User user) + { + var info = new ItemImageInfo + { + Path = user.ProfileImage.Path, + Type = ImageType.Primary, + DateModified = user.ProfileImage.LastModified, + }; + + if (request.Width.HasValue) + { + info.Width = request.Width.Value; + } + + if (request.Height.HasValue) + { + info.Height = request.Height.Value; + } + + return info; + } + /// /// Posts the image. /// @@ -767,5 +870,25 @@ namespace MediaBrowser.Api.Images entity.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); } + + public async Task PostImage(User user, Stream inputStream, string mimeType) + { + using var reader = new StreamReader(inputStream); + var text = await reader.ReadToEndAsync().ConfigureAwait(false); + + var bytes = Convert.FromBase64String(text); + var memoryStream = new MemoryStream(bytes) + { + Position = 0 + }; + + // Handle image/png; charset=utf-8 + mimeType = mimeType.Split(';').FirstOrDefault(); + + await _providerManager + .SaveImage(user, memoryStream, mimeType, Path.Combine(ServerConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, _imageProcessor.GetImageCacheTag(user))) + .ConfigureAwait(false); + await _userManager.UpdateUserAsync(user); + } } } diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs index 997b1c45a..783fc6073 100644 --- a/MediaBrowser.Api/Library/LibraryService.cs +++ b/MediaBrowser.Api/Library/LibraryService.cs @@ -755,12 +755,12 @@ namespace MediaBrowser.Api.Library }); } - private void LogDownload(BaseItem item, User user, AuthorizationInfo auth) + private void LogDownload(BaseItem item, Jellyfin.Data.Entities.User user, AuthorizationInfo auth) { try { _activityManager.Create(new Jellyfin.Data.Entities.ActivityLog( - string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Name, item.Name), + string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name), "UserDownloadingContent", auth.UserId, DateTime.UtcNow, @@ -840,7 +840,7 @@ namespace MediaBrowser.Api.Library return baseItemDtos; } - private BaseItem TranslateParentItem(BaseItem item, User user) + private BaseItem TranslateParentItem(BaseItem item, Jellyfin.Data.Entities.User user) { return item.GetParent() is AggregateFolder ? _libraryManager.GetUserRootFolder().GetChildren(user, true) @@ -882,7 +882,7 @@ namespace MediaBrowser.Api.Library return ToOptimizedResult(counts); } - private int GetCount(Type type, User user, GetItemCounts request) + private int GetCount(Type type, Jellyfin.Data.Entities.User user, GetItemCounts request) { var query = new InternalItemsQuery(user) { diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index 5fe4c0cca..279fd6ee9 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -7,6 +7,7 @@ using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Api.UserLibrary; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; @@ -859,7 +860,7 @@ namespace MediaBrowser.Api.LiveTv throw new SecurityException("Anonymous live tv management is not allowed."); } - if (!user.Policy.EnableLiveTvManagement) + if (!user.HasPermission(PermissionKind.EnableLiveTvManagement)) { throw new SecurityException("The current user does not have permission to manage live tv."); } diff --git a/MediaBrowser.Api/Movies/MoviesService.cs b/MediaBrowser.Api/Movies/MoviesService.cs index 46da8b909..b97a0dca1 100644 --- a/MediaBrowser.Api/Movies/MoviesService.cs +++ b/MediaBrowser.Api/Movies/MoviesService.cs @@ -148,7 +148,12 @@ namespace MediaBrowser.Api.Movies return result; } - private IEnumerable GetRecommendationCategories(User user, string parentId, int categoryLimit, int itemLimit, DtoOptions dtoOptions) + private IEnumerable GetRecommendationCategories( + Jellyfin.Data.Entities.User user, + string parentId, + int categoryLimit, + int itemLimit, + DtoOptions dtoOptions) { var categories = new List(); @@ -251,7 +256,12 @@ namespace MediaBrowser.Api.Movies return categories.OrderBy(i => i.RecommendationType); } - private IEnumerable GetWithDirector(User user, IEnumerable names, int itemLimit, DtoOptions dtoOptions, RecommendationType type) + private IEnumerable GetWithDirector( + Jellyfin.Data.Entities.User user, + IEnumerable names, + int itemLimit, + DtoOptions dtoOptions, + RecommendationType type) { var itemTypes = new List { typeof(Movie).Name }; if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions) @@ -293,7 +303,12 @@ namespace MediaBrowser.Api.Movies } } - private IEnumerable GetWithActor(User user, IEnumerable names, int itemLimit, DtoOptions dtoOptions, RecommendationType type) + private IEnumerable GetWithActor( + Jellyfin.Data.Entities.User user, + IEnumerable names, + int itemLimit, + DtoOptions dtoOptions, + RecommendationType type) { var itemTypes = new List { typeof(Movie).Name }; if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions) @@ -334,7 +349,12 @@ namespace MediaBrowser.Api.Movies } } - private IEnumerable GetSimilarTo(User user, List baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type) + private IEnumerable GetSimilarTo( + Jellyfin.Data.Entities.User user, + List baselineItems, + int itemLimit, + DtoOptions dtoOptions, + RecommendationType type) { var itemTypes = new List { typeof(Movie).Name }; if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions) diff --git a/MediaBrowser.Api/Music/InstantMixService.cs b/MediaBrowser.Api/Music/InstantMixService.cs index cacec8d64..e00b06e38 100644 --- a/MediaBrowser.Api/Music/InstantMixService.cs +++ b/MediaBrowser.Api/Music/InstantMixService.cs @@ -171,7 +171,7 @@ namespace MediaBrowser.Api.Music return GetResult(items, user, request, dtoOptions); } - private object GetResult(List items, User user, BaseGetSimilarItems request, DtoOptions dtoOptions) + private object GetResult(List items, Jellyfin.Data.Entities.User user, BaseGetSimilarItems request, DtoOptions dtoOptions) { var list = items; diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 928ca1612..65f6ccd4d 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; @@ -196,7 +197,7 @@ namespace MediaBrowser.Api.Playback if (state.VideoRequest != null && !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { var auth = AuthorizationContext.GetAuthorizationInfo(Request); - if (auth.User != null && !auth.User.Policy.EnableVideoPlaybackTranscoding) + if (auth.User != null && !auth.User.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)) { ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state); diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs index db24eaca6..0ef39c2d7 100644 --- a/MediaBrowser.Api/Playback/MediaInfoService.cs +++ b/MediaBrowser.Api/Playback/MediaInfoService.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; @@ -400,21 +401,24 @@ namespace MediaBrowser.Api.Playback if (item is Audio) { - Logger.LogInformation("User policy for {0}. EnableAudioPlaybackTranscoding: {1}", user.Name, user.Policy.EnableAudioPlaybackTranscoding); + Logger.LogInformation( + "User policy for {0}. EnableAudioPlaybackTranscoding: {1}", + user.Username, + user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)); } else { Logger.LogInformation("User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}", - user.Name, - user.Policy.EnablePlaybackRemuxing, - user.Policy.EnableVideoPlaybackTranscoding, - user.Policy.EnableAudioPlaybackTranscoding); + user.Username, + user.HasPermission(PermissionKind.EnablePlaybackRemuxing), + user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding), + user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)); } // Beginning of Playback Determination: Attempt DirectPlay first if (mediaSource.SupportsDirectPlay) { - if (mediaSource.IsRemote && user.Policy.ForceRemoteSourceTranscoding) + if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) { mediaSource.SupportsDirectPlay = false; } @@ -428,14 +432,16 @@ namespace MediaBrowser.Api.Playback if (item is Audio) { - if (!user.Policy.EnableAudioPlaybackTranscoding) + if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) { options.ForceDirectPlay = true; } } else if (item is Video) { - if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing) + if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) + && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) + && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) { options.ForceDirectPlay = true; } @@ -463,7 +469,7 @@ namespace MediaBrowser.Api.Playback if (mediaSource.SupportsDirectStream) { - if (mediaSource.IsRemote && user.Policy.ForceRemoteSourceTranscoding) + if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) { mediaSource.SupportsDirectStream = false; } @@ -473,14 +479,16 @@ namespace MediaBrowser.Api.Playback if (item is Audio) { - if (!user.Policy.EnableAudioPlaybackTranscoding) + if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) { options.ForceDirectStream = true; } } else if (item is Video) { - if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing) + if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) + && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) + && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) { options.ForceDirectStream = true; } @@ -512,7 +520,7 @@ namespace MediaBrowser.Api.Playback ? streamBuilder.BuildAudioItem(options) : streamBuilder.BuildVideoItem(options); - if (mediaSource.IsRemote && user.Policy.ForceRemoteSourceTranscoding) + if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) { if (streamInfo != null) { @@ -576,10 +584,10 @@ namespace MediaBrowser.Api.Playback } } - private long? GetMaxBitrate(long? clientMaxBitrate, User user) + private long? GetMaxBitrate(long? clientMaxBitrate, Jellyfin.Data.Entities.User user) { var maxBitrate = clientMaxBitrate; - var remoteClientMaxBitrate = user?.Policy.RemoteClientBitrateLimit ?? 0; + var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0; if (remoteClientMaxBitrate <= 0) { diff --git a/MediaBrowser.Api/Sessions/SessionService.cs b/MediaBrowser.Api/Sessions/SessionService.cs index 020bb5042..d986eea65 100644 --- a/MediaBrowser.Api/Sessions/SessionService.cs +++ b/MediaBrowser.Api/Sessions/SessionService.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; @@ -326,12 +327,12 @@ namespace MediaBrowser.Api.Sessions var user = _userManager.GetUserById(request.ControllableByUserId); - if (!user.Policy.EnableRemoteControlOfOtherUsers) + if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)) { result = result.Where(i => i.UserId.Equals(Guid.Empty) || i.ContainsUser(request.ControllableByUserId)); } - if (!user.Policy.EnableSharedDeviceControl) + if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl)) { result = result.Where(i => !i.UserId.Equals(Guid.Empty)); } diff --git a/MediaBrowser.Api/SuggestionsService.cs b/MediaBrowser.Api/SuggestionsService.cs index 91f85db6f..51fa1eb71 100644 --- a/MediaBrowser.Api/SuggestionsService.cs +++ b/MediaBrowser.Api/SuggestionsService.cs @@ -78,7 +78,7 @@ namespace MediaBrowser.Api }; } - private QueryResult GetItems(GetSuggestedItems request, User user, DtoOptions dtoOptions) + private QueryResult GetItems(GetSuggestedItems request, Jellyfin.Data.Entities.User user, DtoOptions dtoOptions) { return _libraryManager.GetItemsResult(new InternalItemsQuery(user) { diff --git a/MediaBrowser.Api/TvShowsService.cs b/MediaBrowser.Api/TvShowsService.cs index cd8e8dfbe..0c23d8b29 100644 --- a/MediaBrowser.Api/TvShowsService.cs +++ b/MediaBrowser.Api/TvShowsService.cs @@ -378,7 +378,7 @@ namespace MediaBrowser.Api { var user = _userManager.GetUserById(request.UserId); - var series = GetSeries(request.Id, user); + var series = GetSeries(request.Id); if (series == null) { @@ -404,7 +404,7 @@ namespace MediaBrowser.Api }; } - private Series GetSeries(string seriesId, User user) + private Series GetSeries(string seriesId) { if (!string.IsNullOrWhiteSpace(seriesId)) { @@ -433,7 +433,7 @@ namespace MediaBrowser.Api } else if (request.Season.HasValue) { - var series = GetSeries(request.Id, user); + var series = GetSeries(request.Id); if (series == null) { @@ -446,7 +446,7 @@ namespace MediaBrowser.Api } else { - var series = GetSeries(request.Id, user); + var series = GetSeries(request.Id); if (series == null) { diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs index c4a52d5f5..75a33350e 100644 --- a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs +++ b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs @@ -94,7 +94,7 @@ namespace MediaBrowser.Api.UserLibrary { var dtoOptions = GetDtoOptions(AuthorizationContext, request); - User user = null; + Jellyfin.Data.Entities.User user = null; BaseItem parentItem; if (!request.UserId.Equals(Guid.Empty)) @@ -246,7 +246,7 @@ namespace MediaBrowser.Api.UserLibrary { var dtoOptions = GetDtoOptions(AuthorizationContext, request); - User user = null; + Jellyfin.Data.Entities.User user = null; BaseItem parentItem; if (!request.UserId.Equals(Guid.Empty)) diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index c4d44042b..cbceb3625 100644 --- a/MediaBrowser.Api/UserLibrary/ItemsService.cs +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -86,7 +87,7 @@ namespace MediaBrowser.Api.UserLibrary var ancestorIds = Array.Empty(); - var excludeFolderIds = user.Configuration.LatestItemsExcludes; + var excludeFolderIds = user.GetPreference(PreferenceKind.LatestItemExcludes); if (parentIdGuid.Equals(Guid.Empty) && excludeFolderIds.Length > 0) { ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true) @@ -179,7 +180,7 @@ namespace MediaBrowser.Api.UserLibrary /// /// Gets the items to serialize. /// - private QueryResult GetQueryResult(GetItems request, DtoOptions dtoOptions, User user) + private QueryResult GetQueryResult(GetItems request, DtoOptions dtoOptions, Jellyfin.Data.Entities.User user) { if (string.Equals(request.IncludeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase) || string.Equals(request.IncludeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase)) @@ -211,14 +212,14 @@ namespace MediaBrowser.Api.UserLibrary request.IncludeItemTypes = "Playlist"; } - bool isInEnabledFolder = user.Policy.EnabledFolders.Any(i => new Guid(i) == item.Id) + bool isInEnabledFolder = user.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id) // Assume all folders inside an EnabledChannel are enabled - || user.Policy.EnabledChannels.Any(i => new Guid(i) == item.Id); + || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id); var collectionFolders = _libraryManager.GetCollectionFolders(item); foreach (var collectionFolder in collectionFolders) { - if (user.Policy.EnabledFolders.Contains( + if (user.GetPreference(PreferenceKind.EnabledFolders).Contains( collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase)) { @@ -226,9 +227,12 @@ namespace MediaBrowser.Api.UserLibrary } } - if (!(item is UserRootFolder) && !user.Policy.EnableAllFolders && !isInEnabledFolder && !user.Policy.EnableAllChannels) + if (!(item is UserRootFolder) + && !isInEnabledFolder + && !user.HasPermission(PermissionKind.EnableAllFolders) + && !user.HasPermission(PermissionKind.EnableAllChannels)) { - Logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Name, item.Name); + Logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name); return new QueryResult { Items = Array.Empty(), @@ -251,7 +255,7 @@ namespace MediaBrowser.Api.UserLibrary }; } - private InternalItemsQuery GetItemsQuery(GetItems request, DtoOptions dtoOptions, User user) + private InternalItemsQuery GetItemsQuery(GetItems request, DtoOptions dtoOptions, Jellyfin.Data.Entities.User user) { var query = new InternalItemsQuery(user) { diff --git a/MediaBrowser.Api/UserLibrary/PlaystateService.cs b/MediaBrowser.Api/UserLibrary/PlaystateService.cs index d0faca163..fb8bda190 100644 --- a/MediaBrowser.Api/UserLibrary/PlaystateService.cs +++ b/MediaBrowser.Api/UserLibrary/PlaystateService.cs @@ -437,7 +437,7 @@ namespace MediaBrowser.Api.UserLibrary /// if set to true [was played]. /// The date played. /// Task. - private UserItemDataDto UpdatePlayedStatus(User user, string itemId, bool wasPlayed, DateTime? datePlayed) + private UserItemDataDto UpdatePlayedStatus(Jellyfin.Data.Entities.User user, string itemId, bool wasPlayed, DateTime? datePlayed) { var item = _libraryManager.GetItemById(itemId); diff --git a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs index 7fa750adb..f75852885 100644 --- a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs +++ b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs @@ -312,7 +312,7 @@ namespace MediaBrowser.Api.UserLibrary if (!request.IsPlayed.HasValue) { - if (user.Configuration.HidePlayedInLatest) + if (user.HidePlayedInLatest) { request.IsPlayed = false; } diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs index 78fc6c694..7bf4f88f4 100644 --- a/MediaBrowser.Api/UserService.cs +++ b/MediaBrowser.Api/UserService.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; @@ -300,12 +301,12 @@ namespace MediaBrowser.Api if (request.IsDisabled.HasValue) { - users = users.Where(i => i.Policy.IsDisabled == request.IsDisabled.Value); + users = users.Where(i => i.HasPermission(PermissionKind.IsDisabled) == request.IsDisabled.Value); } if (request.IsHidden.HasValue) { - users = users.Where(i => i.Policy.IsHidden == request.IsHidden.Value); + users = users.Where(i => i.HasPermission(PermissionKind.IsHidden) == request.IsHidden.Value); } if (filterByDevice) @@ -322,12 +323,12 @@ namespace MediaBrowser.Api { if (!_networkManager.IsInLocalNetwork(Request.RemoteIp)) { - users = users.Where(i => i.Policy.EnableRemoteAccess); + users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess)); } } var result = users - .OrderBy(u => u.Name) + .OrderBy(u => u.Username) .Select(i => _userManager.GetUserDto(i, Request.RemoteIp)) .ToArray(); @@ -397,7 +398,7 @@ namespace MediaBrowser.Api // Password should always be null return Post(new AuthenticateUserByName { - Username = user.Name, + Username = user.Username, Password = null, Pw = request.Pw }); @@ -456,7 +457,12 @@ namespace MediaBrowser.Api } else { - var success = await _userManager.AuthenticateUser(user.Name, request.CurrentPw, request.CurrentPassword, Request.RemoteIp, false).ConfigureAwait(false); + var success = await _userManager.AuthenticateUser( + user.Username, + request.CurrentPw, + request.CurrentPassword, + Request.RemoteIp, + false).ConfigureAwait(false); if (success == null) { @@ -506,10 +512,10 @@ namespace MediaBrowser.Api var user = _userManager.GetUserById(id); - if (string.Equals(user.Name, dtoUser.Name, StringComparison.Ordinal)) + if (string.Equals(user.Username, dtoUser.Name, StringComparison.Ordinal)) { - _userManager.UpdateUser(user); - _userManager.UpdateConfiguration(user, dtoUser.Configuration); + await _userManager.UpdateUserAsync(user); + _userManager.UpdateConfiguration(user.Id, dtoUser.Configuration); } else { @@ -568,24 +574,24 @@ namespace MediaBrowser.Api var user = _userManager.GetUserById(request.Id); // If removing admin access - if (!request.IsAdministrator && user.Policy.IsAdministrator) + if (!request.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator)) { - if (_userManager.Users.Count(i => i.Policy.IsAdministrator) == 1) + if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1) { throw new ArgumentException("There must be at least one user in the system with administrative access."); } } // If disabling - if (request.IsDisabled && user.Policy.IsAdministrator) + if (request.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator)) { throw new ArgumentException("Administrators cannot be disabled."); } // If disabling - if (request.IsDisabled && !user.Policy.IsDisabled) + if (request.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled)) { - if (_userManager.Users.Count(i => !i.Policy.IsDisabled) == 1) + if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1) { throw new ArgumentException("There must be at least one enabled user in the system."); } @@ -594,7 +600,7 @@ namespace MediaBrowser.Api _sessionMananger.RevokeUserTokens(user.Id, currentToken); } - _userManager.UpdateUserPolicy(request.Id, request); + _userManager.UpdatePolicy(request.Id, request); } } } diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs index f5571065f..c0324a384 100644 --- a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs +++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; +using Jellyfin.Data.Entities; using MediaBrowser.Model.Users; namespace MediaBrowser.Controller.Authentication diff --git a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs index 2639960e7..d9b814f69 100644 --- a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs +++ b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; +using Jellyfin.Data.Entities; using MediaBrowser.Model.Users; namespace MediaBrowser.Controller.Authentication diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs index cdf2ca69e..d6a1fc84e 100644 --- a/MediaBrowser.Controller/Channels/Channel.cs +++ b/MediaBrowser.Controller/Channels/Channel.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Linq; using System.Text.Json.Serialization; using System.Threading; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Querying; @@ -11,18 +12,20 @@ namespace MediaBrowser.Controller.Channels { public class Channel : Folder { - public override bool IsVisible(User user) + public override bool IsVisible(Jellyfin.Data.Entities.User user) { - if (user.Policy.BlockedChannels != null) + if (user.GetPreference(PreferenceKind.BlockedChannels) != null) { - if (user.Policy.BlockedChannels.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase)) + if (user.GetPreference(PreferenceKind.BlockedChannels).Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase)) { return false; } } else { - if (!user.Policy.EnableAllChannels && !user.Policy.EnabledChannels.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase)) + if (!user.HasPermission(PermissionKind.EnableAllChannels) + && !user.GetPreference(PreferenceKind.EnabledChannels) + .Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase)) { return false; } @@ -74,7 +77,7 @@ namespace MediaBrowser.Controller.Channels return false; } - internal static bool IsChannelVisible(BaseItem channelItem, User user) + internal static bool IsChannelVisible(BaseItem channelItem, Jellyfin.Data.Entities.User user) { var channel = ChannelManager.GetChannel(channelItem.ChannelId.ToString("")); diff --git a/MediaBrowser.Controller/Collections/ICollectionManager.cs b/MediaBrowser.Controller/Collections/ICollectionManager.cs index cfe8493d3..f51c73bd7 100644 --- a/MediaBrowser.Controller/Collections/ICollectionManager.cs +++ b/MediaBrowser.Controller/Collections/ICollectionManager.cs @@ -51,6 +51,6 @@ namespace MediaBrowser.Controller.Collections /// The items. /// The user. /// IEnumerable{BaseItem}. - IEnumerable CollapseItemsWithinBoxSets(IEnumerable items, User user); + IEnumerable CollapseItemsWithinBoxSets(IEnumerable items, Jellyfin.Data.Entities.User user); } } diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index 77d567631..117af110a 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -1,7 +1,7 @@ using System; using System.IO; using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; +using Jellyfin.Data.Entities; using MediaBrowser.Model.Devices; using MediaBrowser.Model.Events; using MediaBrowser.Model.Querying; diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index 36c746624..bdb12402a 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -47,8 +47,11 @@ namespace MediaBrowser.Controller.Drawing /// The image. /// Guid. string GetImageCacheTag(BaseItem item, ItemImageInfo image); + string GetImageCacheTag(BaseItem item, ChapterInfo info); + string GetImageCacheTag(Jellyfin.Data.Entities.User user); + /// /// Processes the image. /// diff --git a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs index df9050de5..9f505be93 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs @@ -1,5 +1,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Entities; +using User = Jellyfin.Data.Entities.User; namespace MediaBrowser.Controller.Drawing { diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs index ba693a065..5ac4f05c0 100644 --- a/MediaBrowser.Controller/Dto/IDtoService.cs +++ b/MediaBrowser.Controller/Dto/IDtoService.cs @@ -38,7 +38,7 @@ namespace MediaBrowser.Controller.Dto /// The fields. /// The user. /// The owner. - BaseItemDto GetBaseItemDto(BaseItem item, ItemFields[] fields, User user = null, BaseItem owner = null); + BaseItemDto GetBaseItemDto(BaseItem item, ItemFields[] fields, Jellyfin.Data.Entities.User user = null, BaseItem owner = null); /// /// Gets the base item dto. @@ -48,7 +48,7 @@ namespace MediaBrowser.Controller.Dto /// The user. /// The owner. /// BaseItemDto. - BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null); + BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, Jellyfin.Data.Entities.User user = null, BaseItem owner = null); /// /// Gets the base item dtos. @@ -57,11 +57,11 @@ namespace MediaBrowser.Controller.Dto /// The options. /// The user. /// The owner. - IReadOnlyList GetBaseItemDtos(IReadOnlyList items, DtoOptions options, User user = null, BaseItem owner = null); + IReadOnlyList GetBaseItemDtos(IReadOnlyList items, DtoOptions options, Jellyfin.Data.Entities.User user = null, BaseItem owner = null); /// /// Gets the item by name dto. /// - BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List taggedItems, User user = null); + BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List taggedItems, Jellyfin.Data.Entities.User user = null); } } diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index a700d0be4..9065cb27f 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index c216176e7..fbadeafad 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -77,7 +79,7 @@ namespace MediaBrowser.Controller.Entities.Audio [JsonIgnore] public IEnumerable /// The user. /// PlayAccess. - public PlayAccess GetPlayAccess(User user) + public PlayAccess GetPlayAccess(Jellyfin.Data.Entities.User user) { - if (!user.Policy.EnableMediaPlayback) + if (!user.HasPermission(PermissionKind.EnableMediaPlayback)) { return PlayAccess.None; } @@ -1760,7 +1761,7 @@ namespace MediaBrowser.Controller.Entities /// The user. /// true if [is parental allowed] [the specified user]; otherwise, false. /// user - public bool IsParentalAllowed(User user) + public bool IsParentalAllowed(Jellyfin.Data.Entities.User user) { if (user == null) { @@ -1772,7 +1773,7 @@ namespace MediaBrowser.Controller.Entities return false; } - var maxAllowedRating = user.Policy.MaxParentalRating; + var maxAllowedRating = user.MaxParentalAgeRating; if (maxAllowedRating == null) { @@ -1788,7 +1789,7 @@ namespace MediaBrowser.Controller.Entities if (string.IsNullOrEmpty(rating)) { - return !GetBlockUnratedValue(user.Policy); + return !GetBlockUnratedValue(user); } var value = LocalizationManager.GetRatingLevel(rating); @@ -1796,7 +1797,7 @@ namespace MediaBrowser.Controller.Entities // Could not determine the integer value if (!value.HasValue) { - var isAllowed = !GetBlockUnratedValue(user.Policy); + var isAllowed = !GetBlockUnratedValue(user); if (!isAllowed) { @@ -1856,10 +1857,9 @@ namespace MediaBrowser.Controller.Entities return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); } - private bool IsVisibleViaTags(User user) + private bool IsVisibleViaTags(Jellyfin.Data.Entities.User user) { - var policy = user.Policy; - if (policy.BlockedTags.Any(i => Tags.Contains(i, StringComparer.OrdinalIgnoreCase))) + if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => Tags.Contains(i, StringComparer.OrdinalIgnoreCase))) { return false; } @@ -1885,22 +1885,18 @@ namespace MediaBrowser.Controller.Entities /// /// Gets the block unrated value. /// - /// The configuration. + /// The configuration. /// true if XXXX, false otherwise. - protected virtual bool GetBlockUnratedValue(UserPolicy config) + protected virtual bool GetBlockUnratedValue(Jellyfin.Data.Entities.User user) { // Don't block plain folders that are unrated. Let the media underneath get blocked // Special folders like series and albums will override this method. - if (IsFolder) - { - return false; - } - if (this is IItemByName) + if (IsFolder || this is IItemByName) { return false; } - return config.BlockUnratedItems.Contains(GetBlockUnratedType()); + return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType().ToString()); } /// @@ -1910,7 +1906,7 @@ namespace MediaBrowser.Controller.Entities /// The user. /// true if the specified user is visible; otherwise, false. /// user - public virtual bool IsVisible(User user) + public virtual bool IsVisible(Jellyfin.Data.Entities.User user) { if (user == null) { @@ -1920,7 +1916,7 @@ namespace MediaBrowser.Controller.Entities return IsParentalAllowed(user); } - public virtual bool IsVisibleStandalone(User user) + public virtual bool IsVisibleStandalone(Jellyfin.Data.Entities.User user) { if (SourceType == SourceType.Channel) { @@ -1933,7 +1929,7 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public virtual bool SupportsInheritedParentImages => false; - protected bool IsVisibleStandaloneInternal(User user, bool checkFolders) + protected bool IsVisibleStandaloneInternal(Jellyfin.Data.Entities.User user, bool checkFolders) { if (!IsVisible(user)) { @@ -2130,7 +2126,8 @@ namespace MediaBrowser.Controller.Entities /// if set to true [reset position]. /// Task. /// - public virtual void MarkPlayed(User user, + public virtual void MarkPlayed( + Jellyfin.Data.Entities.User user, DateTime? datePlayed, bool resetPosition) { @@ -2167,7 +2164,7 @@ namespace MediaBrowser.Controller.Entities /// The user. /// Task. /// - public virtual void MarkUnplayed(User user) + public virtual void MarkUnplayed(Jellyfin.Data.Entities.User user) { if (user == null) { @@ -2543,21 +2540,21 @@ namespace MediaBrowser.Controller.Entities UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); } - public virtual bool IsPlayed(User user) + public virtual bool IsPlayed(Jellyfin.Data.Entities.User user) { var userdata = UserDataManager.GetUserData(user, this); return userdata != null && userdata.Played; } - public bool IsFavoriteOrLiked(User user) + public bool IsFavoriteOrLiked(Jellyfin.Data.Entities.User user) { var userdata = UserDataManager.GetUserData(user, this); return userdata != null && (userdata.IsFavorite || (userdata.Likes ?? false)); } - public virtual bool IsUnplayed(User user) + public virtual bool IsUnplayed(Jellyfin.Data.Entities.User user) { if (user == null) { @@ -2623,7 +2620,7 @@ namespace MediaBrowser.Controller.Entities return path; } - public virtual void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields) + public virtual void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, Jellyfin.Data.Entities.User user, DtoOptions fields) { if (RunTimeTicks.HasValue) { @@ -2736,14 +2733,14 @@ namespace MediaBrowser.Controller.Entities return RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken); } - public string GetEtag(User user) + public string GetEtag(Jellyfin.Data.Entities.User user) { var list = GetEtagValues(user); return string.Join("|", list).GetMD5().ToString("N", CultureInfo.InvariantCulture); } - protected virtual List GetEtagValues(User user) + protected virtual List GetEtagValues(Jellyfin.Data.Entities.User user) { return new List { diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs index dcad2554b..c4a2929dc 100644 --- a/MediaBrowser.Controller/Entities/Book.cs +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; diff --git a/MediaBrowser.Controller/Entities/DayOfWeekHelper.cs b/MediaBrowser.Controller/Entities/DayOfWeekHelper.cs deleted file mode 100644 index 8a79e0783..000000000 --- a/MediaBrowser.Controller/Entities/DayOfWeekHelper.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Generic; -using MediaBrowser.Model.Configuration; - -namespace MediaBrowser.Controller.Entities -{ - public static class DayOfWeekHelper - { - public static List GetDaysOfWeek(DynamicDayOfWeek day) - { - return GetDaysOfWeek(new List { day }); - } - - public static List GetDaysOfWeek(List days) - { - var list = new List(); - - if (days.Contains(DynamicDayOfWeek.Sunday) || - days.Contains(DynamicDayOfWeek.Weekend) || - days.Contains(DynamicDayOfWeek.Everyday)) - { - list.Add(DayOfWeek.Sunday); - } - - if (days.Contains(DynamicDayOfWeek.Saturday) || - days.Contains(DynamicDayOfWeek.Weekend) || - days.Contains(DynamicDayOfWeek.Everyday)) - { - list.Add(DayOfWeek.Saturday); - } - - if (days.Contains(DynamicDayOfWeek.Monday) || - days.Contains(DynamicDayOfWeek.Weekday) || - days.Contains(DynamicDayOfWeek.Everyday)) - { - list.Add(DayOfWeek.Monday); - } - - if (days.Contains(DynamicDayOfWeek.Tuesday) || - days.Contains(DynamicDayOfWeek.Weekday) || - days.Contains(DynamicDayOfWeek.Everyday)) - { - list.Add(DayOfWeek.Tuesday - ); - } - - if (days.Contains(DynamicDayOfWeek.Wednesday) || - days.Contains(DynamicDayOfWeek.Weekday) || - days.Contains(DynamicDayOfWeek.Everyday)) - { - list.Add(DayOfWeek.Wednesday); - } - - if (days.Contains(DynamicDayOfWeek.Thursday) || - days.Contains(DynamicDayOfWeek.Weekday) || - days.Contains(DynamicDayOfWeek.Everyday)) - { - list.Add(DayOfWeek.Thursday); - } - - if (days.Contains(DynamicDayOfWeek.Friday) || - days.Contains(DynamicDayOfWeek.Weekday) || - days.Contains(DynamicDayOfWeek.Everyday)) - { - list.Add(DayOfWeek.Friday); - } - - return list; - } - } -} diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index a468e0c35..03644b0c6 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Collections; @@ -173,23 +174,25 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public IEnumerable RecursiveChildren => GetRecursiveChildren(); - public override bool IsVisible(User user) + public override bool IsVisible(Jellyfin.Data.Entities.User user) { if (this is ICollectionFolder && !(this is BasePluginFolder)) { - if (user.Policy.BlockedMediaFolders != null) + if (user.GetPreference(PreferenceKind.BlockedMediaFolders) != null) { - if (user.Policy.BlockedMediaFolders.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase) || + if (user.GetPreference(PreferenceKind.BlockedMediaFolders).Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase) || // Backwards compatibility - user.Policy.BlockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase)) + user.GetPreference(PreferenceKind.BlockedMediaFolders).Contains(Name, StringComparer.OrdinalIgnoreCase)) { return false; } } else { - if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase)) + if (!user.HasPermission(PermissionKind.EnableAllFolders) + && !user.GetPreference(PreferenceKind.EnabledFolders) + .Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase)) { return false; } @@ -583,7 +586,7 @@ namespace MediaBrowser.Controller.Entities }); } - public virtual int GetChildCount(User user) + public virtual int GetChildCount(Jellyfin.Data.Entities.User user) { if (LinkedChildren.Length > 0) { @@ -608,7 +611,7 @@ namespace MediaBrowser.Controller.Entities return result.TotalRecordCount; } - public virtual int GetRecursiveChildCount(User user) + public virtual int GetRecursiveChildCount(Jellyfin.Data.Entities.User user) { return GetItems(new InternalItemsQuery(user) { @@ -877,7 +880,7 @@ namespace MediaBrowser.Controller.Entities try { query.Parent = this; - query.ChannelIds = new Guid[] { ChannelId }; + query.ChannelIds = new[] { ChannelId }; // Don't blow up here because it could cause parent screens with other content to fail return ChannelManager.GetChannelItemsInternal(query, new SimpleProgress(), CancellationToken.None).Result; @@ -947,11 +950,13 @@ namespace MediaBrowser.Controller.Entities return UserViewBuilder.SortAndPage(items, null, query, LibraryManager, enableSorting); } - private static IEnumerable CollapseBoxSetItemsIfNeeded(IEnumerable items, + private static IEnumerable CollapseBoxSetItemsIfNeeded( + IEnumerable items, InternalItemsQuery query, BaseItem queryParent, - User user, - IServerConfigurationManager configurationManager, ICollectionManager collectionManager) + Jellyfin.Data.Entities.User user, + IServerConfigurationManager configurationManager, + ICollectionManager collectionManager) { if (items == null) { @@ -968,7 +973,7 @@ namespace MediaBrowser.Controller.Entities private static bool CollapseBoxSetItems(InternalItemsQuery query, BaseItem queryParent, - User user, + Jellyfin.Data.Entities.User user, IServerConfigurationManager configurationManager) { // Could end up stuck in a loop like this @@ -1191,7 +1196,7 @@ namespace MediaBrowser.Controller.Entities return true; } - public List GetChildren(User user, bool includeLinkedChildren) + public List GetChildren(Jellyfin.Data.Entities.User user, bool includeLinkedChildren) { if (user == null) { @@ -1201,7 +1206,7 @@ namespace MediaBrowser.Controller.Entities return GetChildren(user, includeLinkedChildren, null); } - public virtual List GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + public virtual List GetChildren(Jellyfin.Data.Entities.User user, bool includeLinkedChildren, InternalItemsQuery query) { if (user == null) { @@ -1221,7 +1226,7 @@ namespace MediaBrowser.Controller.Entities return result.Values.ToList(); } - protected virtual IEnumerable GetEligibleChildrenForRecursiveChildren(User user) + protected virtual IEnumerable GetEligibleChildrenForRecursiveChildren(Jellyfin.Data.Entities.User user) { return Children; } @@ -1230,7 +1235,7 @@ namespace MediaBrowser.Controller.Entities /// Adds the children to list. /// /// true if XXXX, false otherwise - private void AddChildren(User user, bool includeLinkedChildren, Dictionary result, bool recursive, InternalItemsQuery query) + private void AddChildren(Jellyfin.Data.Entities.User user, bool includeLinkedChildren, Dictionary result, bool recursive, InternalItemsQuery query) { foreach (var child in GetEligibleChildrenForRecursiveChildren(user)) { @@ -1279,12 +1284,12 @@ namespace MediaBrowser.Controller.Entities /// if set to true [include linked children]. /// IEnumerable{BaseItem}. /// - public IEnumerable GetRecursiveChildren(User user, bool includeLinkedChildren = true) + public IEnumerable GetRecursiveChildren(Jellyfin.Data.Entities.User user, bool includeLinkedChildren = true) { return GetRecursiveChildren(user, null); } - public virtual IEnumerable GetRecursiveChildren(User user, InternalItemsQuery query) + public virtual IEnumerable GetRecursiveChildren(Jellyfin.Data.Entities.User user, InternalItemsQuery query) { if (user == null) { @@ -1403,7 +1408,7 @@ namespace MediaBrowser.Controller.Entities return false; } - public List GetLinkedChildren(User user) + public List GetLinkedChildren(Jellyfin.Data.Entities.User user) { if (!FilterLinkedChildrenPerUser || user == null) { @@ -1565,7 +1570,7 @@ namespace MediaBrowser.Controller.Entities /// The date played. /// if set to true [reset position]. /// Task. - public override void MarkPlayed(User user, + public override void MarkPlayed(Jellyfin.Data.Entities.User user, DateTime? datePlayed, bool resetPosition) { @@ -1577,7 +1582,7 @@ namespace MediaBrowser.Controller.Entities EnableTotalRecordCount = false }; - if (!user.Configuration.DisplayMissingEpisodes) + if (!user.DisplayMissingEpisodes) { query.IsVirtualItem = false; } @@ -1606,7 +1611,7 @@ namespace MediaBrowser.Controller.Entities /// /// The user. /// Task. - public override void MarkUnplayed(User user) + public override void MarkUnplayed(Jellyfin.Data.Entities.User user) { var itemsResult = GetItemList(new InternalItemsQuery { @@ -1624,7 +1629,7 @@ namespace MediaBrowser.Controller.Entities } } - public override bool IsPlayed(User user) + public override bool IsPlayed(Jellyfin.Data.Entities.User user) { var itemsResult = GetItemList(new InternalItemsQuery(user) { @@ -1639,7 +1644,7 @@ namespace MediaBrowser.Controller.Entities .All(i => i.IsPlayed(user)); } - public override bool IsUnplayed(User user) + public override bool IsUnplayed(Jellyfin.Data.Entities.User user) { return !IsPlayed(user); } @@ -1684,7 +1689,7 @@ namespace MediaBrowser.Controller.Entities } } - public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields) + public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, Jellyfin.Data.Entities.User user, DtoOptions fields) { if (!SupportsUserDataFromChildren) { diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index bd96059e3..6a2cafcba 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; @@ -15,7 +16,7 @@ namespace MediaBrowser.Controller.Entities public int? Limit { get; set; } - public User User { get; set; } + public Jellyfin.Data.Entities.User User { get; set; } public BaseItem SimilarTo { get; set; } @@ -213,25 +214,26 @@ namespace MediaBrowser.Controller.Entities Years = Array.Empty(); } - public InternalItemsQuery(User user) + public InternalItemsQuery(Jellyfin.Data.Entities.User user) : this() { SetUser(user); } - public void SetUser(User user) + public void SetUser(Jellyfin.Data.Entities.User user) { if (user != null) { - var policy = user.Policy; - MaxParentalRating = policy.MaxParentalRating; + MaxParentalRating = user.MaxParentalAgeRating; - if (policy.MaxParentalRating.HasValue) + if (MaxParentalRating.HasValue) { - BlockUnratedItems = policy.BlockUnratedItems.Where(i => i != UnratedItem.Other).ToArray(); + BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems) + .Where(i => i != UnratedItem.Other.ToString()) + .Select(e => Enum.Parse(e, true)).ToArray(); } - ExcludeInheritedTags = policy.BlockedTags; + ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags); User = user; } diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index feaf8c45a..1c1bde3e4 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -2,11 +2,10 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Users; namespace MediaBrowser.Controller.Entities.Movies { @@ -45,9 +44,9 @@ namespace MediaBrowser.Controller.Entities.Movies /// The display order. public string DisplayOrder { get; set; } - protected override bool GetBlockUnratedValue(UserPolicy config) + protected override bool GetBlockUnratedValue(Jellyfin.Data.Entities.User user) { - return config.BlockUnratedItems.Contains(UnratedItem.Movie); + return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Movie.ToString()); } public override double GetDefaultPrimaryImageAspectRatio() @@ -101,7 +100,7 @@ namespace MediaBrowser.Controller.Entities.Movies [JsonIgnore] public override bool IsPreSorted => true; - public override bool IsAuthorizedToDelete(User user, List allCollectionFolders) + public override bool IsAuthorizedToDelete(Jellyfin.Data.Entities.User user, List allCollectionFolders) { return true; } @@ -111,7 +110,7 @@ namespace MediaBrowser.Controller.Entities.Movies return true; } - public override List GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + public override List GetChildren(Jellyfin.Data.Entities.User user, bool includeLinkedChildren, InternalItemsQuery query) { var children = base.GetChildren(user, includeLinkedChildren, query); @@ -131,7 +130,7 @@ namespace MediaBrowser.Controller.Entities.Movies return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList(); } - public override IEnumerable GetRecursiveChildren(User user, InternalItemsQuery query) + public override IEnumerable GetRecursiveChildren(Jellyfin.Data.Entities.User user, InternalItemsQuery query) { var children = base.GetRecursiveChildren(user, query); @@ -149,7 +148,7 @@ namespace MediaBrowser.Controller.Entities.Movies return GetItemLookupInfo(); } - public override bool IsVisible(User user) + public override bool IsVisible(Jellyfin.Data.Entities.User user) { if (IsLegacyBoxSet) { @@ -177,7 +176,7 @@ namespace MediaBrowser.Controller.Entities.Movies return false; } - public override bool IsVisibleStandalone(User user) + public override bool IsVisibleStandalone(Jellyfin.Data.Entities.User user) { if (IsLegacyBoxSet) { @@ -189,7 +188,7 @@ namespace MediaBrowser.Controller.Entities.Movies public Guid[] LibraryFolderIds { get; set; } - private Guid[] GetLibraryFolderIds(User user) + private Guid[] GetLibraryFolderIds(Jellyfin.Data.Entities.User user) { return LibraryManager.GetUserRootFolder().GetChildren(user, true) .Select(i => i.Id) diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index 11dc472b6..38359afcc 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Controller/Entities/MusicVideo.cs b/MediaBrowser.Controller/Entities/MusicVideo.cs index 603242063..1b9d4614e 100644 --- a/MediaBrowser.Controller/Entities/MusicVideo.cs +++ b/MediaBrowser.Controller/Entities/MusicVideo.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 49229fa4b..0a89da46d 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 9c8a469e2..0d1fec62f 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; @@ -60,7 +61,7 @@ namespace MediaBrowser.Controller.Entities.TV return list; } - public override int GetChildCount(User user) + public override int GetChildCount(Jellyfin.Data.Entities.User user) { var result = GetChildren(user, true).Count; @@ -143,17 +144,17 @@ namespace MediaBrowser.Controller.Entities.TV /// /// Gets the episodes. /// - public List GetEpisodes(User user, DtoOptions options) + public List GetEpisodes(Jellyfin.Data.Entities.User user, DtoOptions options) { return GetEpisodes(Series, user, options); } - public List GetEpisodes(Series series, User user, DtoOptions options) + public List GetEpisodes(Series series, Jellyfin.Data.Entities.User user, DtoOptions options) { return GetEpisodes(series, user, null, options); } - public List GetEpisodes(Series series, User user, IEnumerable allSeriesEpisodes, DtoOptions options) + public List GetEpisodes(Series series, Jellyfin.Data.Entities.User user, IEnumerable allSeriesEpisodes, DtoOptions options) { return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options); } @@ -163,12 +164,12 @@ namespace MediaBrowser.Controller.Entities.TV return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true)); } - public override List GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + public override List GetChildren(Jellyfin.Data.Entities.User user, bool includeLinkedChildren, InternalItemsQuery query) { return GetEpisodes(user, new DtoOptions(true)); } - protected override bool GetBlockUnratedValue(UserPolicy config) + protected override bool GetBlockUnratedValue(Jellyfin.Data.Entities.User config) { // Don't block. Let either the entire series rating or episode rating determine it return false; @@ -203,7 +204,7 @@ namespace MediaBrowser.Controller.Entities.TV public Guid FindSeriesId() { var series = FindParent(); - return series == null ? Guid.Empty : series.Id; + return series?.Id ?? Guid.Empty; } /// @@ -234,7 +235,7 @@ namespace MediaBrowser.Controller.Entities.TV if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path)) { - IndexNumber = IndexNumber ?? LibraryManager.GetSeasonNumberFromPath(Path); + IndexNumber ??= LibraryManager.GetSeasonNumberFromPath(Path); // If a change was made record it if (IndexNumber.HasValue) diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 2475b2b7e..4aed5fbdc 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -5,13 +5,12 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Users; namespace MediaBrowser.Controller.Entities.TV { @@ -111,7 +110,7 @@ namespace MediaBrowser.Controller.Entities.TV return series.GetPresentationUniqueKey(); } - public override int GetChildCount(User user) + public override int GetChildCount(Jellyfin.Data.Entities.User user) { var seriesKey = GetUniqueSeriesKey(this); @@ -119,7 +118,7 @@ namespace MediaBrowser.Controller.Entities.TV { AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, - IncludeItemTypes = new[] { typeof(Season).Name }, + IncludeItemTypes = new[] { nameof(Season) }, IsVirtualItem = false, Limit = 0, DtoOptions = new DtoOptions(false) @@ -131,7 +130,7 @@ namespace MediaBrowser.Controller.Entities.TV return result; } - public override int GetRecursiveChildCount(User user) + public override int GetRecursiveChildCount(Jellyfin.Data.Entities.User user) { var seriesKey = GetUniqueSeriesKey(this); @@ -179,12 +178,12 @@ namespace MediaBrowser.Controller.Entities.TV return list; } - public override List GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + public override List GetChildren(Jellyfin.Data.Entities.User user, bool includeLinkedChildren, InternalItemsQuery query) { return GetSeasons(user, new DtoOptions(true)); } - public List GetSeasons(User user, DtoOptions options) + public List GetSeasons(Jellyfin.Data.Entities.User user, DtoOptions options) { var query = new InternalItemsQuery(user) { @@ -196,7 +195,7 @@ namespace MediaBrowser.Controller.Entities.TV return LibraryManager.GetItemList(query); } - private void SetSeasonQueryOptions(InternalItemsQuery query, User user) + private void SetSeasonQueryOptions(InternalItemsQuery query, Jellyfin.Data.Entities.User user) { var seriesKey = GetUniqueSeriesKey(this); @@ -205,14 +204,9 @@ namespace MediaBrowser.Controller.Entities.TV query.IncludeItemTypes = new[] { typeof(Season).Name }; query.OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple(i, SortOrder.Ascending)).ToArray(); - if (user != null) + if (user != null && !user.DisplayMissingEpisodes) { - var config = user.Configuration; - - if (!config.DisplayMissingEpisodes) - { - query.IsMissing = false; - } + query.IsMissing = false; } } @@ -245,7 +239,7 @@ namespace MediaBrowser.Controller.Entities.TV return LibraryManager.GetItemsResult(query); } - public IEnumerable GetEpisodes(User user, DtoOptions options) + public IEnumerable GetEpisodes(Jellyfin.Data.Entities.User user, DtoOptions options) { var seriesKey = GetUniqueSeriesKey(this); @@ -257,8 +251,8 @@ namespace MediaBrowser.Controller.Entities.TV OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple(i, SortOrder.Ascending)).ToArray(), DtoOptions = options }; - var config = user.Configuration; - if (!config.DisplayMissingEpisodes) + + if (!user.DisplayMissingEpisodes) { query.IsMissing = false; } @@ -311,7 +305,7 @@ namespace MediaBrowser.Controller.Entities.TV // Refresh episodes and other children foreach (var item in items) { - if ((item is Season)) + if (item is Season) { continue; } @@ -351,7 +345,7 @@ namespace MediaBrowser.Controller.Entities.TV await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false); } - public List GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options) + public List GetSeasonEpisodes(Season parentSeason, Jellyfin.Data.Entities.User user, DtoOptions options) { var queryFromSeries = ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons; @@ -370,8 +364,7 @@ namespace MediaBrowser.Controller.Entities.TV }; if (user != null) { - var config = user.Configuration; - if (!config.DisplayMissingEpisodes) + if (!user.DisplayMissingEpisodes) { query.IsMissing = false; } @@ -382,7 +375,7 @@ namespace MediaBrowser.Controller.Entities.TV return GetSeasonEpisodes(parentSeason, user, allItems, options); } - public List GetSeasonEpisodes(Season parentSeason, User user, IEnumerable allSeriesEpisodes, DtoOptions options) + public List GetSeasonEpisodes(Season parentSeason, Jellyfin.Data.Entities.User user, IEnumerable allSeriesEpisodes, DtoOptions options) { if (allSeriesEpisodes == null) { @@ -452,9 +445,9 @@ namespace MediaBrowser.Controller.Entities.TV } - protected override bool GetBlockUnratedValue(UserPolicy config) + protected override bool GetBlockUnratedValue(Jellyfin.Data.Entities.User user) { - return config.BlockUnratedItems.Contains(UnratedItem.Series); + return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Series.ToString()); } public override UnratedItem GetBlockUnratedType() diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs index 0b8be90cd..c646e8ae6 100644 --- a/MediaBrowser.Controller/Entities/Trailer.cs +++ b/MediaBrowser.Controller/Entities/Trailer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Controller/Entities/User.cs b/MediaBrowser.Controller/Entities/User.cs deleted file mode 100644 index 53601a610..000000000 --- a/MediaBrowser.Controller/Entities/User.cs +++ /dev/null @@ -1,262 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Text.Json.Serialization; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Users; - -namespace MediaBrowser.Controller.Entities -{ - /// - /// Class User - /// - public class User : BaseItem - { - public static IUserManager UserManager { get; set; } - - /// - /// Gets or sets the password. - /// - /// The password. - public string Password { get; set; } - public string EasyPassword { get; set; } - - // Strictly to remove JsonIgnore - public override ItemImageInfo[] ImageInfos - { - get => base.ImageInfos; - set => base.ImageInfos = value; - } - - /// - /// Gets or sets the path. - /// - /// The path. - [JsonIgnore] - public override string Path - { - get => ConfigurationDirectoryPath; - set => base.Path = value; - } - - private string _name; - /// - /// Gets or sets the name. - /// - /// The name. - public override string Name - { - get => _name; - set - { - _name = value; - - // lazy load this again - SortName = null; - } - } - - /// - /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself - /// - /// The containing folder path. - [JsonIgnore] - public override string ContainingFolderPath => Path; - - /// - /// Gets the root folder. - /// - /// The root folder. - [JsonIgnore] - public Folder RootFolder => LibraryManager.GetUserRootFolder(); - - /// - /// Gets or sets the last login date. - /// - /// The last login date. - public DateTime? LastLoginDate { get; set; } - /// - /// Gets or sets the last activity date. - /// - /// The last activity date. - public DateTime? LastActivityDate { get; set; } - - private volatile UserConfiguration _config; - private readonly object _configSyncLock = new object(); - [JsonIgnore] - public UserConfiguration Configuration - { - get - { - if (_config == null) - { - lock (_configSyncLock) - { - if (_config == null) - { - _config = UserManager.GetUserConfiguration(this); - } - } - } - - return _config; - } - set => _config = value; - } - - private volatile UserPolicy _policy; - private readonly object _policySyncLock = new object(); - [JsonIgnore] - public UserPolicy Policy - { - get - { - if (_policy == null) - { - lock (_policySyncLock) - { - if (_policy == null) - { - _policy = UserManager.GetUserPolicy(this); - } - } - } - - return _policy; - } - set => _policy = value; - } - - /// - /// Renames the user. - /// - /// The new name. - /// Task. - /// - public Task Rename(string newName) - { - if (string.IsNullOrWhiteSpace(newName)) - { - throw new ArgumentException("Username can't be empty", nameof(newName)); - } - - Name = newName; - - return RefreshMetadata( - new MetadataRefreshOptions(new DirectoryService(FileSystem)) - { - ReplaceAllMetadata = true, - ImageRefreshMode = MetadataRefreshMode.FullRefresh, - MetadataRefreshMode = MetadataRefreshMode.FullRefresh, - ForceSave = true - - }, - CancellationToken.None); - } - - public override void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken) - { - UserManager.UpdateUser(this); - } - - /// - /// Gets the path to the user's configuration directory - /// - /// The configuration directory path. - [JsonIgnore] - public string ConfigurationDirectoryPath => GetConfigurationDirectoryPath(Name); - - public override double GetDefaultPrimaryImageAspectRatio() - { - return 1; - } - - /// - /// Gets the configuration directory path. - /// - /// The username. - /// System.String. - private string GetConfigurationDirectoryPath(string username) - { - var parentPath = ConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath; - - // TODO: Remove idPath and just use usernamePath for future releases - var usernamePath = System.IO.Path.Combine(parentPath, username); - var idPath = System.IO.Path.Combine(parentPath, Id.ToString("N", CultureInfo.InvariantCulture)); - if (!Directory.Exists(usernamePath) && Directory.Exists(idPath)) - { - Directory.Move(idPath, usernamePath); - } - - return usernamePath; - } - - public bool IsParentalScheduleAllowed() - { - return IsParentalScheduleAllowed(DateTime.UtcNow); - } - - public bool IsParentalScheduleAllowed(DateTime date) - { - var schedules = Policy.AccessSchedules; - - if (schedules.Length == 0) - { - return true; - } - - foreach (var i in schedules) - { - if (IsParentalScheduleAllowed(i, date)) - { - return true; - } - } - return false; - } - - private bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date) - { - if (date.Kind != DateTimeKind.Utc) - { - throw new ArgumentException("Utc date expected"); - } - - var localTime = date.ToLocalTime(); - - return DayOfWeekHelper.GetDaysOfWeek(schedule.DayOfWeek).Contains(localTime.DayOfWeek) && - IsWithinTime(schedule, localTime); - } - - private bool IsWithinTime(AccessSchedule schedule, DateTime localTime) - { - var hour = localTime.TimeOfDay.TotalHours; - - return hour >= schedule.StartHour && hour <= schedule.EndHour; - } - - public bool IsFolderGrouped(Guid id) - { - foreach (var i in Configuration.GroupedFolders) - { - if (new Guid(i) == id) - { - return true; - } - } - return false; - } - - [JsonIgnore] - public override bool SupportsPeople => false; - - public long InternalId { get; set; } - - - } -} diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs index 8a68f830c..9d211540d 100644 --- a/MediaBrowser.Controller/Entities/UserRootFolder.cs +++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs @@ -63,7 +63,7 @@ namespace MediaBrowser.Controller.Entities return UserViewBuilder.SortAndPage(result, null, query, LibraryManager, true); } - public override int GetChildCount(User user) + public override int GetChildCount(Jellyfin.Data.Entities.User user) { return GetChildren(user, true).Count; } @@ -74,7 +74,7 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public override bool IsPreSorted => true; - protected override IEnumerable GetEligibleChildrenForRecursiveChildren(User user) + protected override IEnumerable GetEligibleChildrenForRecursiveChildren(Jellyfin.Data.Entities.User user) { var list = base.GetEligibleChildrenForRecursiveChildren(user).ToList(); list.AddRange(LibraryManager.RootFolder.VirtualChildren); diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index 4ce9ec6f8..b44e7c191 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -48,7 +48,7 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public override bool SupportsPlayedStatus => false; - public override int GetChildCount(User user) + public override int GetChildCount(Jellyfin.Data.Entities.User user) { return GetChildren(user, true).Count; } @@ -70,7 +70,7 @@ namespace MediaBrowser.Controller.Entities .GetUserItems(parent, this, CollectionType, query); } - public override List GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + public override List GetChildren(Jellyfin.Data.Entities.User user, bool includeLinkedChildren, InternalItemsQuery query) { if (query == null) { @@ -93,7 +93,7 @@ namespace MediaBrowser.Controller.Entities return true; } - public override IEnumerable GetRecursiveChildren(User user, InternalItemsQuery query) + public override IEnumerable GetRecursiveChildren(Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.SetUser(user); query.Recursive = true; @@ -103,7 +103,7 @@ namespace MediaBrowser.Controller.Entities return GetItemList(query); } - protected override IEnumerable GetEligibleChildrenForRecursiveChildren(User user) + protected override IEnumerable GetEligibleChildrenForRecursiveChildren(Jellyfin.Data.Entities.User user) { return GetChildren(user, false); } diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 435a1e8da..0ad8e6b71 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -125,7 +125,7 @@ namespace MediaBrowser.Controller.Entities return 50; } - private QueryResult GetMovieFolders(Folder parent, User user, InternalItemsQuery query) + private QueryResult GetMovieFolders(Folder parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { if (query.Recursive) { @@ -140,19 +140,20 @@ namespace MediaBrowser.Controller.Entities return parent.QueryRecursive(query); } - var list = new List(); - - list.Add(GetUserView(SpecialFolder.MovieResume, "HeaderContinueWatching", "0", parent)); - list.Add(GetUserView(SpecialFolder.MovieLatest, "Latest", "1", parent)); - list.Add(GetUserView(SpecialFolder.MovieMovies, "Movies", "2", parent)); - list.Add(GetUserView(SpecialFolder.MovieCollections, "Collections", "3", parent)); - list.Add(GetUserView(SpecialFolder.MovieFavorites, "Favorites", "4", parent)); - list.Add(GetUserView(SpecialFolder.MovieGenres, "Genres", "5", parent)); + var list = new List + { + GetUserView(SpecialFolder.MovieResume, "HeaderContinueWatching", "0", parent), + GetUserView(SpecialFolder.MovieLatest, "Latest", "1", parent), + GetUserView(SpecialFolder.MovieMovies, "Movies", "2", parent), + GetUserView(SpecialFolder.MovieCollections, "Collections", "3", parent), + GetUserView(SpecialFolder.MovieFavorites, "Favorites", "4", parent), + GetUserView(SpecialFolder.MovieGenres, "Genres", "5", parent) + }; return GetResult(list, parent, query); } - private QueryResult GetFavoriteMovies(Folder parent, User user, InternalItemsQuery query) + private QueryResult GetFavoriteMovies(Folder parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; @@ -163,7 +164,7 @@ namespace MediaBrowser.Controller.Entities return _libraryManager.GetItemsResult(query); } - private QueryResult GetFavoriteSeries(Folder parent, User user, InternalItemsQuery query) + private QueryResult GetFavoriteSeries(Folder parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; @@ -174,7 +175,7 @@ namespace MediaBrowser.Controller.Entities return _libraryManager.GetItemsResult(query); } - private QueryResult GetFavoriteEpisodes(Folder parent, User user, InternalItemsQuery query) + private QueryResult GetFavoriteEpisodes(Folder parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; @@ -185,7 +186,7 @@ namespace MediaBrowser.Controller.Entities return _libraryManager.GetItemsResult(query); } - private QueryResult GetMovieMovies(Folder parent, User user, InternalItemsQuery query) + private QueryResult GetMovieMovies(Folder parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; @@ -196,7 +197,7 @@ namespace MediaBrowser.Controller.Entities return _libraryManager.GetItemsResult(query); } - private QueryResult GetMovieCollections(Folder parent, User user, InternalItemsQuery query) + private QueryResult GetMovieCollections(Folder parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.Parent = null; query.IncludeItemTypes = new[] { typeof(BoxSet).Name }; @@ -206,7 +207,7 @@ namespace MediaBrowser.Controller.Entities return _libraryManager.GetItemsResult(query); } - private QueryResult GetMovieLatest(Folder parent, User user, InternalItemsQuery query) + private QueryResult GetMovieLatest(Folder parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.OrderBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName }.Select(i => new ValueTuple(i, SortOrder.Descending)).ToArray(); @@ -219,7 +220,7 @@ namespace MediaBrowser.Controller.Entities return ConvertToResult(_libraryManager.GetItemList(query)); } - private QueryResult GetMovieResume(Folder parent, User user, InternalItemsQuery query) + private QueryResult GetMovieResume(Folder parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.SortName }.Select(i => new ValueTuple(i, SortOrder.Descending)).ToArray(); query.IsResumable = true; @@ -242,7 +243,7 @@ namespace MediaBrowser.Controller.Entities }; } - private QueryResult GetMovieGenres(Folder parent, User user, InternalItemsQuery query) + private QueryResult GetMovieGenres(Folder parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { var genres = parent.QueryRecursive(new InternalItemsQuery(user) { @@ -272,7 +273,7 @@ namespace MediaBrowser.Controller.Entities return GetResult(genres, parent, query); } - private QueryResult GetMovieGenreItems(Folder queryParent, Folder displayParent, User user, InternalItemsQuery query) + private QueryResult GetMovieGenreItems(Folder queryParent, Folder displayParent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = queryParent; @@ -284,7 +285,7 @@ namespace MediaBrowser.Controller.Entities return _libraryManager.GetItemsResult(query); } - private QueryResult GetTvView(Folder parent, User user, InternalItemsQuery query) + private QueryResult GetTvView(Folder parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { if (query.Recursive) { @@ -293,26 +294,32 @@ namespace MediaBrowser.Controller.Entities if (query.IncludeItemTypes.Length == 0) { - query.IncludeItemTypes = new[] { typeof(Series).Name, typeof(Season).Name, typeof(Episode).Name }; + query.IncludeItemTypes = new[] + { + nameof(Series), + nameof(Season), + nameof(Episode) + }; } return parent.QueryRecursive(query); } - var list = new List(); - - list.Add(GetUserView(SpecialFolder.TvResume, "HeaderContinueWatching", "0", parent)); - list.Add(GetUserView(SpecialFolder.TvNextUp, "HeaderNextUp", "1", parent)); - list.Add(GetUserView(SpecialFolder.TvLatest, "Latest", "2", parent)); - list.Add(GetUserView(SpecialFolder.TvShowSeries, "Shows", "3", parent)); - list.Add(GetUserView(SpecialFolder.TvFavoriteSeries, "HeaderFavoriteShows", "4", parent)); - list.Add(GetUserView(SpecialFolder.TvFavoriteEpisodes, "HeaderFavoriteEpisodes", "5", parent)); - list.Add(GetUserView(SpecialFolder.TvGenres, "Genres", "6", parent)); + var list = new List + { + GetUserView(SpecialFolder.TvResume, "HeaderContinueWatching", "0", parent), + GetUserView(SpecialFolder.TvNextUp, "HeaderNextUp", "1", parent), + GetUserView(SpecialFolder.TvLatest, "Latest", "2", parent), + GetUserView(SpecialFolder.TvShowSeries, "Shows", "3", parent), + GetUserView(SpecialFolder.TvFavoriteSeries, "HeaderFavoriteShows", "4", parent), + GetUserView(SpecialFolder.TvFavoriteEpisodes, "HeaderFavoriteEpisodes", "5", parent), + GetUserView(SpecialFolder.TvGenres, "Genres", "6", parent) + }; return GetResult(list, parent, query); } - private QueryResult GetTvLatest(Folder parent, User user, InternalItemsQuery query) + private QueryResult GetTvLatest(Folder parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.OrderBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName }.Select(i => new ValueTuple(i, SortOrder.Descending)).ToArray(); @@ -341,7 +348,7 @@ namespace MediaBrowser.Controller.Entities return result; } - private QueryResult GetTvResume(Folder parent, User user, InternalItemsQuery query) + private QueryResult GetTvResume(Folder parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.SortName }.Select(i => new ValueTuple(i, SortOrder.Descending)).ToArray(); query.IsResumable = true; @@ -354,7 +361,7 @@ namespace MediaBrowser.Controller.Entities return ConvertToResult(_libraryManager.GetItemList(query)); } - private QueryResult GetTvSeries(Folder parent, User user, InternalItemsQuery query) + private QueryResult GetTvSeries(Folder parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = parent; @@ -365,7 +372,7 @@ namespace MediaBrowser.Controller.Entities return _libraryManager.GetItemsResult(query); } - private QueryResult GetTvGenres(Folder parent, User user, InternalItemsQuery query) + private QueryResult GetTvGenres(Folder parent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { var genres = parent.QueryRecursive(new InternalItemsQuery(user) { @@ -395,7 +402,7 @@ namespace MediaBrowser.Controller.Entities return GetResult(genres, parent, query); } - private QueryResult GetTvGenreItems(Folder queryParent, Folder displayParent, User user, InternalItemsQuery query) + private QueryResult GetTvGenreItems(Folder queryParent, Folder displayParent, Jellyfin.Data.Entities.User user, InternalItemsQuery query) { query.Recursive = true; query.Parent = queryParent; @@ -417,7 +424,8 @@ namespace MediaBrowser.Controller.Entities }; } - private QueryResult GetResult(IEnumerable items, + private QueryResult GetResult( + IEnumerable items, BaseItem queryParent, InternalItemsQuery query) where T : BaseItem @@ -484,7 +492,7 @@ namespace MediaBrowser.Controller.Entities }; } - public static bool Filter(BaseItem item, User user, InternalItemsQuery query, IUserDataManager userDataManager, ILibraryManager libraryManager) + public static bool Filter(BaseItem item, Jellyfin.Data.Entities.User user, InternalItemsQuery query, IUserDataManager userDataManager, ILibraryManager libraryManager) { if (query.MediaTypes.Length > 0 && !query.MediaTypes.Contains(item.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) { @@ -942,7 +950,7 @@ namespace MediaBrowser.Controller.Entities return true; } - private IEnumerable GetMediaFolders(User user) + private IEnumerable GetMediaFolders(Jellyfin.Data.Entities.User user) { if (user == null) { @@ -957,7 +965,7 @@ namespace MediaBrowser.Controller.Entities .Where(i => user.IsFolderGrouped(i.Id) && UserView.IsEligibleForGrouping(i)); } - private BaseItem[] GetMediaFolders(User user, IEnumerable viewTypes) + private BaseItem[] GetMediaFolders(Jellyfin.Data.Entities.User user, IEnumerable viewTypes) { if (user == null) { @@ -978,7 +986,7 @@ namespace MediaBrowser.Controller.Entities }).ToArray(); } - private BaseItem[] GetMediaFolders(Folder parent, User user, IEnumerable viewTypes) + private BaseItem[] GetMediaFolders(Folder parent, Jellyfin.Data.Entities.User user, IEnumerable viewTypes) { if (parent == null || parent is UserView) { diff --git a/MediaBrowser.Controller/Library/IIntroProvider.cs b/MediaBrowser.Controller/Library/IIntroProvider.cs index d9d1ca8c7..aa7001611 100644 --- a/MediaBrowser.Controller/Library/IIntroProvider.cs +++ b/MediaBrowser.Controller/Library/IIntroProvider.cs @@ -15,7 +15,7 @@ namespace MediaBrowser.Controller.Library /// The item. /// The user. /// IEnumerable{System.String}. - Task> GetIntros(BaseItem item, User user); + Task> GetIntros(BaseItem item, Jellyfin.Data.Entities.User user); /// /// Gets all intro files. diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 2e1c97f67..ada570bfd 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -141,7 +141,7 @@ namespace MediaBrowser.Controller.Library /// The item. /// The user. /// IEnumerable{System.String}. - Task> GetIntros(BaseItem item, User user); + Task> GetIntros(BaseItem item, Jellyfin.Data.Entities.User user); /// /// Gets all intro files. @@ -172,8 +172,8 @@ namespace MediaBrowser.Controller.Library /// The sort by. /// The sort order. /// IEnumerable{BaseItem}. - IEnumerable Sort(IEnumerable items, User user, IEnumerable sortBy, SortOrder sortOrder); - IEnumerable Sort(IEnumerable items, User user, IEnumerable> orderBy); + IEnumerable Sort(IEnumerable items, Jellyfin.Data.Entities.User user, IEnumerable sortBy, SortOrder sortOrder); + IEnumerable Sort(IEnumerable items, Jellyfin.Data.Entities.User user, IEnumerable> orderBy); /// /// Gets the user root folder. @@ -284,7 +284,8 @@ namespace MediaBrowser.Controller.Library /// The parent identifier. /// Type of the view. /// Name of the sort. - UserView GetNamedView(User user, + UserView GetNamedView( + Jellyfin.Data.Entities.User user, string name, Guid parentId, string viewType, @@ -297,7 +298,8 @@ namespace MediaBrowser.Controller.Library /// The name. /// Type of the view. /// Name of the sort. - UserView GetNamedView(User user, + UserView GetNamedView( + Jellyfin.Data.Entities.User user, string name, string viewType, string sortName); diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs index 0ceabd0e6..57368778a 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs @@ -55,12 +55,12 @@ namespace MediaBrowser.Controller.Library /// /// Gets the playack media sources. /// - Task> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken); + Task> GetPlaybackMediaSources(BaseItem item, Jellyfin.Data.Entities.User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken); /// /// Gets the static media sources. /// - List GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null); + List GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, Jellyfin.Data.Entities.User user = null); /// /// Gets the static media source. @@ -100,7 +100,7 @@ namespace MediaBrowser.Controller.Library MediaProtocol GetPathProtocol(string path); - void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user); + void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, Jellyfin.Data.Entities.User user); Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProbeDelay, bool isLiveStream, CancellationToken cancellationToken); diff --git a/MediaBrowser.Controller/Library/IMusicManager.cs b/MediaBrowser.Controller/Library/IMusicManager.cs index 554dd0895..0618837bc 100644 --- a/MediaBrowser.Controller/Library/IMusicManager.cs +++ b/MediaBrowser.Controller/Library/IMusicManager.cs @@ -10,16 +10,16 @@ namespace MediaBrowser.Controller.Library /// /// Gets the instant mix from song. /// - List GetInstantMixFromItem(BaseItem item, User user, DtoOptions dtoOptions); + List GetInstantMixFromItem(BaseItem item, Jellyfin.Data.Entities.User user, DtoOptions dtoOptions); /// /// Gets the instant mix from artist. /// - List GetInstantMixFromArtist(MusicArtist artist, User user, DtoOptions dtoOptions); + List GetInstantMixFromArtist(MusicArtist artist, Jellyfin.Data.Entities.User user, DtoOptions dtoOptions); /// /// Gets the instant mix from genre. /// - List GetInstantMixFromGenres(IEnumerable genres, User user, DtoOptions dtoOptions); + List GetInstantMixFromGenres(IEnumerable genres, Jellyfin.Data.Entities.User user, DtoOptions dtoOptions); } } diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs index eb735d31a..15da560ef 100644 --- a/MediaBrowser.Controller/Library/IUserDataManager.cs +++ b/MediaBrowser.Controller/Library/IUserDataManager.cs @@ -27,18 +27,18 @@ namespace MediaBrowser.Controller.Library /// The reason. /// The cancellation token. void SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken); - void SaveUserData(User userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken); + void SaveUserData(Jellyfin.Data.Entities.User user, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken); - UserItemData GetUserData(User user, BaseItem item); + UserItemData GetUserData(Jellyfin.Data.Entities.User user, BaseItem item); UserItemData GetUserData(Guid userId, BaseItem item); /// /// Gets the user data dto. /// - UserItemDataDto GetUserDataDto(BaseItem item, User user); + UserItemDataDto GetUserDataDto(BaseItem item, Jellyfin.Data.Entities.User user); - UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto itemDto, User user, DtoOptions dto_options); + UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto itemDto, Jellyfin.Data.Entities.User user, DtoOptions dto_options); /// /// Get all user data for the given user diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index be7b4ce59..1e385dcb9 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Events; @@ -17,36 +16,41 @@ namespace MediaBrowser.Controller.Library public interface IUserManager { /// - /// Gets the users. + /// Occurs when a user is updated. /// - /// The users. - IEnumerable Users { get; } + event EventHandler> OnUserUpdated; /// - /// Gets the user ids. + /// Occurs when a user is created. /// - /// The users ids. - IEnumerable UsersIds { get; } + event EventHandler> OnUserCreated; /// - /// Occurs when [user updated]. + /// Occurs when a user is deleted. /// - event EventHandler> UserUpdated; + event EventHandler> OnUserDeleted; /// - /// Occurs when [user deleted]. + /// Occurs when a user's password is changed. /// - event EventHandler> UserDeleted; - - event EventHandler> UserCreated; + event EventHandler> OnUserPasswordChanged; - event EventHandler> UserPolicyUpdated; - - event EventHandler> UserConfigurationUpdated; + /// + /// Occurs when a user is locked out. + /// + event EventHandler> OnUserLockedOut; - event EventHandler> UserPasswordChanged; + /// + /// Gets the users. + /// + /// The users. + IEnumerable Users { get; } - event EventHandler> UserLockedOut; + /// + /// Gets the user ids. + /// + /// The users ids. + IEnumerable UsersIds { get; } /// /// Gets a user by Id. @@ -63,13 +67,6 @@ namespace MediaBrowser.Controller.Library /// User. User GetUserByName(string name); - /// - /// Refreshes metadata for each user - /// - /// The cancellation token. - /// Task. - Task RefreshUsersMetadata(CancellationToken cancellationToken); - /// /// Renames the user. /// @@ -89,19 +86,27 @@ namespace MediaBrowser.Controller.Library void UpdateUser(User user); /// - /// Creates the user. + /// Updates the user. /// - /// The name. - /// User. + /// The user. + /// If user is null. + /// If the provided user doesn't exist. + /// A task representing the update of the user. + Task UpdateUserAsync(User user); + + /// + /// Creates a user with the specified name. + /// + /// The name of the new user. + /// The created user. /// name /// User CreateUser(string name); /// - /// Deletes the user. + /// Deletes the specified user. /// - /// The user. - /// Task. + /// The user to be deleted. void DeleteUser(User user); /// @@ -111,13 +116,6 @@ namespace MediaBrowser.Controller.Library /// Task. Task ResetPassword(User user); - /// - /// Gets the offline user dto. - /// - /// The user. - /// UserDto. - UserDto GetOfflineUserDto(User user); - /// /// Resets the easy password. /// @@ -163,47 +161,28 @@ namespace MediaBrowser.Controller.Library /// true if XXXX, false otherwise. Task RedeemPasswordResetPin(string pin); - /// - /// Gets the user policy. - /// - /// The user. - /// UserPolicy. - UserPolicy GetUserPolicy(User user); - - /// - /// Gets the user configuration. - /// - /// The user. - /// UserConfiguration. - UserConfiguration GetUserConfiguration(User user); + void AddParts(IEnumerable authenticationProviders, IEnumerable passwordResetProviders); - /// - /// Updates the configuration. - /// - /// The user identifier. - /// The new configuration. - /// Task. - void UpdateConfiguration(Guid userId, UserConfiguration newConfiguration); + NameIdPair[] GetAuthenticationProviders(); - void UpdateConfiguration(User user, UserConfiguration newConfiguration); + NameIdPair[] GetPasswordResetProviders(); /// - /// Updates the user policy. + /// This method updates the user's configuration. + /// This is only included as a stopgap until the new API, using this internally is not recommended. + /// Instead, modify the user object directlu, then call . /// - /// The user identifier. - /// The user policy. - void UpdateUserPolicy(Guid userId, UserPolicy userPolicy); + /// The user's Id. + /// The request containing the new user configuration. + void UpdateConfiguration(Guid userId, UserConfiguration config); /// - /// Makes the valid username. + /// This method updates the user's policy. + /// This is only included as a stopgap until the new API, using this internally is not recommended. + /// Instead, modify the user object directlu, then call . /// - /// The username. - /// System.String. - string MakeValidUsername(string username); - - void AddParts(IEnumerable authenticationProviders, IEnumerable passwordResetProviders); - - NameIdPair[] GetAuthenticationProviders(); - NameIdPair[] GetPasswordResetProviders(); + /// The user's Id. + /// The request containing the new user policy. + void UpdatePolicy(Guid userId, UserPolicy policy); } } diff --git a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs index b0302d04c..83c0e3297 100644 --- a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs +++ b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs @@ -11,7 +11,7 @@ namespace MediaBrowser.Controller.Library /// public class PlaybackProgressEventArgs : EventArgs { - public List Users { get; set; } + public List Users { get; set; } public long? PlaybackPositionTicks { get; set; } public BaseItem Item { get; set; } public BaseItemDto MediaInfo { get; set; } @@ -28,7 +28,7 @@ namespace MediaBrowser.Controller.Library public PlaybackProgressEventArgs() { - Users = new List(); + Users = new List(); } } } diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index e02c387e4..99fd18bf9 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -115,7 +115,7 @@ namespace MediaBrowser.Controller.LiveTv /// The cancellation token. /// The user. /// Task{ProgramInfoDto}. - Task GetProgram(string id, CancellationToken cancellationToken, User user = null); + Task GetProgram(string id, CancellationToken cancellationToken, Jellyfin.Data.Entities.User user = null); /// /// Gets the programs. @@ -202,7 +202,7 @@ namespace MediaBrowser.Controller.LiveTv /// Gets the enabled users. /// /// IEnumerable{User}. - IEnumerable GetEnabledUsers(); + IEnumerable GetEnabledUsers(); /// /// Gets the internal channels. @@ -221,7 +221,7 @@ namespace MediaBrowser.Controller.LiveTv /// The fields. /// The user. /// Task. - Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> programs, ItemFields[] fields, User user = null); + Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> programs, ItemFields[] fields, Jellyfin.Data.Entities.User user = null); /// /// Saves the tuner host. @@ -258,7 +258,7 @@ namespace MediaBrowser.Controller.LiveTv /// The items. /// The options. /// The user. - void AddChannelInfo(IReadOnlyCollection<(BaseItemDto, LiveTvChannel)> items, DtoOptions options, User user); + void AddChannelInfo(IReadOnlyCollection<(BaseItemDto, LiveTvChannel)> items, DtoOptions options, Jellyfin.Data.Entities.User user); Task> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken); Task> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken); @@ -277,9 +277,9 @@ namespace MediaBrowser.Controller.LiveTv ActiveRecordingInfo GetActiveRecordingInfo(string path); - void AddInfoToRecordingDto(BaseItem item, BaseItemDto dto, ActiveRecordingInfo activeRecordingInfo, User user = null); + void AddInfoToRecordingDto(BaseItem item, BaseItemDto dto, ActiveRecordingInfo activeRecordingInfo, Jellyfin.Data.Entities.User user = null); - List GetRecordingFolders(User user); + List GetRecordingFolders(Jellyfin.Data.Entities.User user); } public class ActiveRecordingInfo diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs index 60391bb83..f3ff8bd04 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs index 13df85aed..e6dc4bdda 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 61a330675..2559bc248 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Text; using System.Threading; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Extensions; using MediaBrowser.Model.Configuration; @@ -1991,7 +1992,7 @@ namespace MediaBrowser.Controller.MediaEncoding filters.Add("hwupload"); } - // When the input may or may not be hardware QSV decodable + // When the input may or may not be hardware QSV decodable else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) { if (!hasTextSubs) @@ -2147,7 +2148,7 @@ namespace MediaBrowser.Controller.MediaEncoding var user = state.User; // If the user doesn't have access to transcoding, then force stream copy, regardless of whether it will be compatible or not - if (user != null && !user.Policy.EnableVideoPlaybackTranscoding) + if (user != null && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)) { state.OutputVideoCodec = "copy"; } @@ -2163,7 +2164,7 @@ namespace MediaBrowser.Controller.MediaEncoding var user = state.User; // If the user doesn't have access to transcoding, then force stream copy, regardless of whether it will be compatible or not - if (user != null && !user.Policy.EnableAudioPlaybackTranscoding) + if (user != null && !user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) { state.OutputAudioCodec = "copy"; } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index 1127a08de..dc4361fc3 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -18,23 +18,38 @@ namespace MediaBrowser.Controller.MediaEncoding public class EncodingJobInfo { public MediaStream VideoStream { get; set; } + public VideoType VideoType { get; set; } + public Dictionary RemoteHttpHeaders { get; set; } + public string OutputVideoCodec { get; set; } + public MediaProtocol InputProtocol { get; set; } + public string MediaPath { get; set; } + public bool IsInputVideo { get; set; } + public IIsoMount IsoMount { get; set; } + public string[] PlayableStreamFileNames { get; set; } + public string OutputAudioCodec { get; set; } + public int? OutputVideoBitrate { get; set; } + public MediaStream SubtitleStream { get; set; } + public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; } + public string[] SupportedSubtitleCodecs { get; set; } public int InternalSubtitleStreamOffset { get; set; } + public MediaSourceInfo MediaSource { get; set; } - public User User { get; set; } + + public Jellyfin.Data.Entities.User User { get; set; } public long? RunTimeTicks { get; set; } diff --git a/MediaBrowser.Controller/Net/AuthorizationInfo.cs b/MediaBrowser.Controller/Net/AuthorizationInfo.cs index 3e004763d..4361e253b 100644 --- a/MediaBrowser.Controller/Net/AuthorizationInfo.cs +++ b/MediaBrowser.Controller/Net/AuthorizationInfo.cs @@ -1,36 +1,40 @@ using System; -using MediaBrowser.Controller.Entities; +using Jellyfin.Data.Entities; namespace MediaBrowser.Controller.Net { public class AuthorizationInfo { /// - /// Gets or sets the user identifier. + /// Gets the user identifier. /// /// The user identifier. - public Guid UserId => User == null ? Guid.Empty : User.Id; + public Guid UserId => User?.Id ?? Guid.Empty; /// /// Gets or sets the device identifier. /// /// The device identifier. public string DeviceId { get; set; } + /// /// Gets or sets the device. /// /// The device. public string Device { get; set; } + /// /// Gets or sets the client. /// /// The client. public string Client { get; set; } + /// /// Gets or sets the version. /// /// The version. public string Version { get; set; } + /// /// Gets or sets the token. /// diff --git a/MediaBrowser.Controller/Net/IAuthService.cs b/MediaBrowser.Controller/Net/IAuthService.cs index 9132404a0..61fc7e6e6 100644 --- a/MediaBrowser.Controller/Net/IAuthService.cs +++ b/MediaBrowser.Controller/Net/IAuthService.cs @@ -9,6 +9,6 @@ namespace MediaBrowser.Controller.Net public interface IAuthService { void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues); - User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtues); + Jellyfin.Data.Entities.User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtues); } } diff --git a/MediaBrowser.Controller/Net/ISessionContext.cs b/MediaBrowser.Controller/Net/ISessionContext.cs index 5c3c19f6b..421ac3fe2 100644 --- a/MediaBrowser.Controller/Net/ISessionContext.cs +++ b/MediaBrowser.Controller/Net/ISessionContext.cs @@ -1,4 +1,4 @@ -using MediaBrowser.Controller.Entities; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Services; diff --git a/MediaBrowser.Controller/Notifications/INotificationService.cs b/MediaBrowser.Controller/Notifications/INotificationService.cs index 8c6019923..2bc751758 100644 --- a/MediaBrowser.Controller/Notifications/INotificationService.cs +++ b/MediaBrowser.Controller/Notifications/INotificationService.cs @@ -25,6 +25,6 @@ namespace MediaBrowser.Controller.Notifications /// /// The user. /// true if [is enabled for user] [the specified user identifier]; otherwise, false. - bool IsEnabledForUser(User user); + bool IsEnabledForUser(Jellyfin.Data.Entities.User user); } } diff --git a/MediaBrowser.Controller/Notifications/UserNotification.cs b/MediaBrowser.Controller/Notifications/UserNotification.cs index 3f46468b3..a1029589b 100644 --- a/MediaBrowser.Controller/Notifications/UserNotification.cs +++ b/MediaBrowser.Controller/Notifications/UserNotification.cs @@ -1,5 +1,5 @@ using System; -using MediaBrowser.Controller.Entities; +using Jellyfin.Data.Entities; using MediaBrowser.Model.Notifications; namespace MediaBrowser.Controller.Notifications diff --git a/MediaBrowser.Controller/Persistence/IUserRepository.cs b/MediaBrowser.Controller/Persistence/IUserRepository.cs deleted file mode 100644 index cd23e5223..000000000 --- a/MediaBrowser.Controller/Persistence/IUserRepository.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using MediaBrowser.Controller.Entities; - -namespace MediaBrowser.Controller.Persistence -{ - /// - /// Provides an interface to implement a User repository - /// - public interface IUserRepository : IRepository - { - /// - /// Deletes the user. - /// - /// The user. - /// Task. - void DeleteUser(User user); - - /// - /// Retrieves all users. - /// - /// IEnumerable{User}. - List RetrieveAllUsers(); - - void CreateUser(User user); - void UpdateUser(User user); - } -} diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index 3b08e72b9..03bdf1eaf 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -78,7 +78,7 @@ namespace MediaBrowser.Controller.Playlists return 1; } - public override bool IsAuthorizedToDelete(User user, List allCollectionFolders) + public override bool IsAuthorizedToDelete(Jellyfin.Data.Entities.User user, List allCollectionFolders) { return true; } @@ -99,7 +99,7 @@ namespace MediaBrowser.Controller.Playlists return Task.CompletedTask; } - public override List GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + public override List GetChildren(Jellyfin.Data.Entities.User user, bool includeLinkedChildren, InternalItemsQuery query) { return GetPlayableItems(user, query); } @@ -109,7 +109,7 @@ namespace MediaBrowser.Controller.Playlists return new List(); } - public override IEnumerable GetRecursiveChildren(User user, InternalItemsQuery query) + public override IEnumerable GetRecursiveChildren(Jellyfin.Data.Entities.User user, InternalItemsQuery query) { return GetPlayableItems(user, query); } @@ -119,7 +119,7 @@ namespace MediaBrowser.Controller.Playlists return GetLinkedChildrenInfos(); } - private List GetPlayableItems(User user, InternalItemsQuery query) + private List GetPlayableItems(Jellyfin.Data.Entities.User user, InternalItemsQuery query) { if (query == null) { @@ -131,7 +131,7 @@ namespace MediaBrowser.Controller.Playlists return base.GetChildren(user, true, query); } - public static List GetPlaylistItems(string playlistMediaType, IEnumerable inputItems, User user, DtoOptions options) + public static List GetPlaylistItems(string playlistMediaType, IEnumerable inputItems, Jellyfin.Data.Entities.User user, DtoOptions options) { if (user != null) { @@ -149,7 +149,7 @@ namespace MediaBrowser.Controller.Playlists return list; } - private static IEnumerable GetPlaylistItems(BaseItem item, User user, string mediaType, DtoOptions options) + private static IEnumerable GetPlaylistItems(BaseItem item, Jellyfin.Data.Entities.User user, string mediaType, DtoOptions options) { if (item is MusicGenre musicGenre) { @@ -222,7 +222,7 @@ namespace MediaBrowser.Controller.Playlists } } - public override bool IsVisible(User user) + public override bool IsVisible(Jellyfin.Data.Entities.User user) { if (!IsSharedItem) { @@ -241,18 +241,10 @@ namespace MediaBrowser.Controller.Playlists } var userId = user.Id.ToString("N", CultureInfo.InvariantCulture); - foreach (var share in shares) - { - if (string.Equals(share.UserId, userId, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; + return shares.Any(share => string.Equals(share.UserId, userId, StringComparison.OrdinalIgnoreCase)); } - public override bool IsVisibleStandalone(User user) + public override bool IsVisibleStandalone(Jellyfin.Data.Entities.User user) { if (!IsSharedItem) { diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs index 254b27460..955db0278 100644 --- a/MediaBrowser.Controller/Providers/IProviderManager.cs +++ b/MediaBrowser.Controller/Providers/IProviderManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -70,6 +71,8 @@ namespace MediaBrowser.Controller.Providers /// Task. Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken); + Task SaveImage(User user, Stream source, string mimeType, string path); + /// /// Adds the metadata providers. /// diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 771027103..32e62db14 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -74,7 +74,7 @@ namespace MediaBrowser.Controller.Session /// Name of the device. /// The remote end point. /// The user. - SessionInfo LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user); + SessionInfo LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, Jellyfin.Data.Entities.User user); void UpdateDeviceName(string sessionId, string reportedDeviceName); diff --git a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs index 1e2df37bf..f079bc7ea 100644 --- a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs +++ b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs @@ -12,7 +12,7 @@ namespace MediaBrowser.Controller.Sorting /// Gets or sets the user. /// /// The user. - User User { get; set; } + Jellyfin.Data.Entities.User User { get; set; } /// /// Gets or sets the user manager. diff --git a/MediaBrowser.Model/Configuration/AccessSchedule.cs b/MediaBrowser.Model/Configuration/AccessSchedule.cs index 120c47dbc..7bd355449 100644 --- a/MediaBrowser.Model/Configuration/AccessSchedule.cs +++ b/MediaBrowser.Model/Configuration/AccessSchedule.cs @@ -1,3 +1,5 @@ +using Jellyfin.Data.Enums; + #pragma warning disable CS1591 namespace MediaBrowser.Model.Configuration diff --git a/MediaBrowser.Model/Configuration/DynamicDayOfWeek.cs b/MediaBrowser.Model/Configuration/DynamicDayOfWeek.cs deleted file mode 100644 index 71b16cfba..000000000 --- a/MediaBrowser.Model/Configuration/DynamicDayOfWeek.cs +++ /dev/null @@ -1,18 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Configuration -{ - public enum DynamicDayOfWeek - { - Sunday = 0, - Monday = 1, - Tuesday = 2, - Wednesday = 3, - Thursday = 4, - Friday = 5, - Saturday = 6, - Everyday = 7, - Weekday = 8, - Weekend = 9 - } -} diff --git a/MediaBrowser.Model/Configuration/SubtitlePlaybackMode.cs b/MediaBrowser.Model/Configuration/SubtitlePlaybackMode.cs deleted file mode 100644 index f0aa2b98c..000000000 --- a/MediaBrowser.Model/Configuration/SubtitlePlaybackMode.cs +++ /dev/null @@ -1,13 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Configuration -{ - public enum SubtitlePlaybackMode - { - Default = 0, - Always = 1, - OnlyForced = 2, - None = 3, - Smart = 4 - } -} diff --git a/MediaBrowser.Model/Configuration/UnratedItem.cs b/MediaBrowser.Model/Configuration/UnratedItem.cs deleted file mode 100644 index e1d1a363d..000000000 --- a/MediaBrowser.Model/Configuration/UnratedItem.cs +++ /dev/null @@ -1,17 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Configuration -{ - public enum UnratedItem - { - Movie, - Trailer, - Series, - Music, - Book, - LiveTvChannel, - LiveTvProgram, - ChannelContent, - Other - } -} diff --git a/MediaBrowser.Model/Configuration/UserConfiguration.cs b/MediaBrowser.Model/Configuration/UserConfiguration.cs index a475c9910..79cf0a065 100644 --- a/MediaBrowser.Model/Configuration/UserConfiguration.cs +++ b/MediaBrowser.Model/Configuration/UserConfiguration.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using Jellyfin.Data.Enums; namespace MediaBrowser.Model.Configuration { diff --git a/MediaBrowser.Model/Notifications/NotificationOptions.cs b/MediaBrowser.Model/Notifications/NotificationOptions.cs index 79a128e9b..b3c46ace2 100644 --- a/MediaBrowser.Model/Notifications/NotificationOptions.cs +++ b/MediaBrowser.Model/Notifications/NotificationOptions.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Users; @@ -109,7 +110,7 @@ namespace MediaBrowser.Model.Notifications !ListHelper.ContainsIgnoreCase(opt.DisabledMonitorUsers, userId.ToString("")); } - public bool IsEnabledToSendToUser(string type, string userId, UserPolicy userPolicy) + public bool IsEnabledToSendToUser(string type, string userId, Jellyfin.Data.Entities.User user) { NotificationOption opt = GetOptions(type); @@ -120,7 +121,7 @@ namespace MediaBrowser.Model.Notifications return true; } - if (opt.SendToUserMode == SendToUserType.Admins && userPolicy.IsAdministrator) + if (opt.SendToUserMode == SendToUserType.Admins && user.HasPermission(PermissionKind.IsAdministrator)) { return true; } diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index ae2b3fd4e..8d94d3971 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -1,7 +1,8 @@ #pragma warning disable CS1591 using System; -using MediaBrowser.Model.Configuration; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; namespace MediaBrowser.Model.Users { @@ -33,7 +34,7 @@ namespace MediaBrowser.Model.Users public string[] BlockedTags { get; set; } public bool EnableUserPreferenceAccess { get; set; } - public AccessSchedule[] AccessSchedules { get; set; } + public Jellyfin.Data.Entities.AccessSchedule[] AccessSchedules { get; set; } public UnratedItem[] BlockUnratedItems { get; set; } public bool EnableRemoteControlOfOtherUsers { get; set; } public bool EnableSharedDeviceControl { get; set; } @@ -77,7 +78,7 @@ namespace MediaBrowser.Model.Users public string[] BlockedChannels { get; set; } public int RemoteClientBitrateLimit { get; set; } - public string AuthenticationProviderId { get; set; } + public string AuthenticatioIsnProviderId { get; set; } public string PasswordResetProviderId { get; set; } public UserPolicy() diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs index 3ab621ba4..6bed38780 100644 --- a/MediaBrowser.Providers/Manager/ImageSaver.cs +++ b/MediaBrowser.Providers/Manager/ImageSaver.cs @@ -5,17 +5,21 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; +using Episode = MediaBrowser.Controller.Entities.TV.Episode; +using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; +using Person = MediaBrowser.Controller.Entities.Person; +using Season = MediaBrowser.Controller.Entities.TV.Season; namespace MediaBrowser.Providers.Manager { @@ -78,11 +82,6 @@ namespace MediaBrowser.Providers.Manager var saveLocally = item.SupportsLocalMetadata && item.IsSaveLocalMetadataEnabled() && !item.ExtraType.HasValue && !(item is Audio); - if (item is User) - { - saveLocally = true; - } - if (type != ImageType.Primary && item is Episode) { saveLocally = false; @@ -136,7 +135,7 @@ namespace MediaBrowser.Providers.Manager var savedPaths = new List(); - using (source) + await using (source) { var currentPathIndex = 0; @@ -172,7 +171,6 @@ namespace MediaBrowser.Providers.Manager } catch (FileNotFoundException) { - } finally { @@ -181,6 +179,16 @@ namespace MediaBrowser.Providers.Manager } } + public async Task SaveImage(User user, Stream source, string mimeType, string path) + { + if (string.IsNullOrEmpty(mimeType)) + { + throw new ArgumentNullException(nameof(mimeType)); + } + + await SaveImageToLocation(source, path, path, CancellationToken.None).ConfigureAwait(false); + } + private async Task SaveImageToLocation(Stream source, string path, string retryPath, CancellationToken cancellationToken) { try @@ -244,7 +252,7 @@ namespace MediaBrowser.Providers.Manager _fileSystem.SetAttributes(path, false, false); - using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous)) + await using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous)) { await source.CopyToAsync(fs, IODefaults.CopyToBufferSize, cancellationToken).ConfigureAwait(false); } @@ -439,7 +447,6 @@ namespace MediaBrowser.Providers.Manager { path = Path.Combine(Path.GetDirectoryName(item.Path), "metadata", filename + extension); } - else if (item.IsInMixedFolder) { path = GetSavePathForItemInMixedFolder(item, type, filename, extension); diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index cfff89767..f4e875a24 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Common.Net; using MediaBrowser.Common.Progress; using MediaBrowser.Controller; @@ -16,7 +17,6 @@ using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Subtitles; @@ -28,6 +28,12 @@ using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; using Priority_Queue; +using Book = MediaBrowser.Controller.Entities.Book; +using Episode = MediaBrowser.Controller.Entities.TV.Episode; +using Movie = MediaBrowser.Controller.Entities.Movies.Movie; +using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; +using Season = MediaBrowser.Controller.Entities.TV.Season; +using Series = MediaBrowser.Controller.Entities.TV.Series; namespace MediaBrowser.Providers.Manager { @@ -182,6 +188,12 @@ namespace MediaBrowser.Providers.Manager return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken); } + public Task SaveImage(User user, Stream source, string mimeType, string path) + { + return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger) + .SaveImage(user, source, mimeType, path); + } + public async Task> GetAvailableRemoteImages(BaseItem item, RemoteImageQuery query, CancellationToken cancellationToken) { var providers = GetRemoteImageProviders(item, query.IncludeDisabledProviders); diff --git a/MediaBrowser.Providers/Users/UserMetadataService.cs b/MediaBrowser.Providers/Users/UserMetadataService.cs deleted file mode 100644 index fb6c91b6c..000000000 --- a/MediaBrowser.Providers/Users/UserMetadataService.cs +++ /dev/null @@ -1,30 +0,0 @@ -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using MediaBrowser.Providers.Manager; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Providers.Users -{ - public class UserMetadataService : MetadataService - { - public UserMetadataService( - IServerConfigurationManager serverConfigurationManager, - ILogger logger, - IProviderManager providerManager, - IFileSystem fileSystem, - ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) - { - } - - /// - protected override void MergeData(MetadataResult source, MetadataResult target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) - { - ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); - } - } -} diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs index 3b3d03c8b..0cb8d8be6 100644 --- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs @@ -7,6 +7,7 @@ using AutoFixture; using AutoFixture.AutoMoq; using Jellyfin.Api.Auth; using Jellyfin.Api.Constants; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Net; using Microsoft.AspNetCore.Authentication; @@ -83,7 +84,7 @@ namespace Jellyfin.Api.Tests.Auth a => a.Authenticate( It.IsAny(), It.IsAny())) - .Returns((User?)null); + .Returns((Jellyfin.Data.Entities.User?)null); var authenticateResult = await _sut.AuthenticateAsync(); @@ -124,7 +125,7 @@ namespace Jellyfin.Api.Tests.Auth var user = SetupUser(); var authenticateResult = await _sut.AuthenticateAsync(); - Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, user.Name)); + Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, user.Username)); } [Theory] @@ -135,7 +136,7 @@ namespace Jellyfin.Api.Tests.Auth var user = SetupUser(isAdmin); var authenticateResult = await _sut.AuthenticateAsync(); - var expectedRole = user.Policy.IsAdministrator ? UserRoles.Administrator : UserRoles.User; + var expectedRole = user.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User; Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Role, expectedRole)); } @@ -148,10 +149,10 @@ namespace Jellyfin.Api.Tests.Auth Assert.Equal(_scheme.Name, authenticatedResult.Ticket.AuthenticationScheme); } - private User SetupUser(bool isAdmin = false) + private Jellyfin.Data.Entities.User SetupUser(bool isAdmin = false) { - var user = _fixture.Create(); - user.Policy.IsAdministrator = isAdmin; + var user = _fixture.Create(); + user.SetPermission(PermissionKind.IsAdministrator, isAdmin); _jellyfinAuthServiceMock.Setup( a => a.Authenticate( -- cgit v1.2.3 From b94afc597c4d51f67552c9ba2c25bdb8df6d8599 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Thu, 14 May 2020 17:13:45 -0400 Subject: Address review comments --- Emby.Server.Implementations/ApplicationHost.cs | 11 --- .../Configuration/ServerConfigurationManager.cs | 6 -- .../Emby.Server.Implementations.csproj | 3 +- Jellyfin.Data/Entities/ActivityLog.cs | 83 +++++++++++----------- Jellyfin.Data/Jellyfin.Data.csproj | 6 +- .../Activity/ActivityManager.cs | 14 ++-- .../Jellyfin.Server.Implementations.csproj | 8 +++ Jellyfin.Server.Implementations/JellyfinDb.cs | 2 +- .../JellyfinDbProvider.cs | 2 +- .../20200502231229_InitialSchema.Designer.cs | 73 ------------------- .../Migrations/20200502231229_InitialSchema.cs | 46 ------------ .../20200514181226_AddActivityLog.Designer.cs | 72 +++++++++++++++++++ .../Migrations/20200514181226_AddActivityLog.cs | 46 ++++++++++++ .../Migrations/DesignTimeJellyfinDbFactory.cs | 7 +- .../Migrations/JellyfinDbModelSnapshot.cs | 4 +- Jellyfin.Server/CoreAppHost.cs | 14 ++++ Jellyfin.Server/Jellyfin.Server.csproj | 8 +-- MediaBrowser.Api/System/ActivityLogService.cs | 13 +++- MediaBrowser.Model/Activity/IActivityManager.cs | 3 +- .../Configuration/ServerConfiguration.cs | 2 - MediaBrowser.Model/Devices/DeviceOptions.cs | 9 +++ MediaBrowser.Model/Devices/DevicesOptions.cs | 23 ------ 22 files changed, 221 insertions(+), 234 deletions(-) delete mode 100644 Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.Designer.cs delete mode 100644 Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.cs create mode 100644 MediaBrowser.Model/Devices/DeviceOptions.cs delete mode 100644 MediaBrowser.Model/Devices/DevicesOptions.cs (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 371b5a5b9..8e5c3c9cf 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -46,8 +46,6 @@ using Emby.Server.Implementations.Session; using Emby.Server.Implementations.SocketSharp; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; -using Jellyfin.Server.Implementations; -using Jellyfin.Server.Implementations.Activity; using MediaBrowser.Api; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; @@ -547,13 +545,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); - // TODO: properly set up scoping and switch to AddDbContextPool - serviceCollection.AddDbContext( - options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"), - ServiceLifetime.Transient); - - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(_fileSystemManager); serviceCollection.AddSingleton(); @@ -664,8 +655,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs index a6eaf2d0a..305e67e8c 100644 --- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -193,12 +193,6 @@ namespace Emby.Server.Implementations.Configuration changed = true; } - if (!config.CameraUploadUpgraded) - { - config.CameraUploadUpgraded = true; - changed = true; - } - if (!config.CollectionsUpgraded) { config.CollectionsUpgraded = true; diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index dccbe2a9a..896e4310e 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -9,7 +9,6 @@ - @@ -51,7 +50,7 @@ - netcoreapp3.1 + netstandard2.1 false true diff --git a/Jellyfin.Data/Entities/ActivityLog.cs b/Jellyfin.Data/Entities/ActivityLog.cs index df3026a77..8fbf6eaab 100644 --- a/Jellyfin.Data/Entities/ActivityLog.cs +++ b/Jellyfin.Data/Entities/ActivityLog.cs @@ -5,34 +5,18 @@ using Microsoft.Extensions.Logging; namespace Jellyfin.Data.Entities { - public partial class ActivityLog + /// + /// An entity referencing an activity log entry. + /// + public partial class ActivityLog : ISavingChanges { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected ActivityLog() - { - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static ActivityLog CreateActivityLogUnsafe() - { - return new ActivityLog(); - } - /// - /// Public constructor with required data + /// Initializes a new instance of the class. + /// Public constructor with required data. /// - /// - /// - /// - /// - /// + /// The name. + /// The type. + /// The user id. public ActivityLog(string name, string type, Guid userId) { if (string.IsNullOrEmpty(name)) @@ -54,14 +38,21 @@ namespace Jellyfin.Data.Entities Init(); } + /// + /// Initializes a new instance of the class. + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected ActivityLog() + { + Init(); + } + /// /// Static create function (for use in LINQ queries, etc.) /// - /// - /// - /// - /// - /// + /// The name. + /// The type. + /// The user's id. public static ActivityLog Create(string name, string type, Guid userId) { return new ActivityLog(name, type, userId); @@ -72,7 +63,8 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Identity, Indexed, Required + /// Gets the identity of this instance. + /// This is the key in the backing database. /// [Key] [Required] @@ -80,7 +72,8 @@ namespace Jellyfin.Data.Entities public int Id { get; protected set; } /// - /// Required, Max length = 512 + /// Gets or sets the name. + /// Required, Max length = 512. /// [Required] [MaxLength(512)] @@ -88,21 +81,24 @@ namespace Jellyfin.Data.Entities public string Name { get; set; } /// - /// Max length = 512 + /// Gets or sets the overview. + /// Max length = 512. /// [MaxLength(512)] [StringLength(512)] public string Overview { get; set; } /// - /// Max length = 512 + /// Gets or sets the short overview. + /// Max length = 512. /// [MaxLength(512)] [StringLength(512)] public string ShortOverview { get; set; } /// - /// Required, Max length = 256 + /// Gets or sets the type. + /// Required, Max length = 256. /// [Required] [MaxLength(256)] @@ -110,41 +106,48 @@ namespace Jellyfin.Data.Entities public string Type { get; set; } /// - /// Required + /// Gets or sets the user id. + /// Required. /// [Required] public Guid UserId { get; set; } /// - /// Max length = 256 + /// Gets or sets the item id. + /// Max length = 256. /// [MaxLength(256)] [StringLength(256)] public string ItemId { get; set; } /// - /// Required + /// Gets or sets the date created. This should be in UTC. + /// Required. /// [Required] public DateTime DateCreated { get; set; } /// - /// Required + /// Gets or sets the log severity. Default is . + /// Required. /// [Required] public LogLevel LogSeverity { get; set; } /// + /// Gets or sets the row version. /// Required, ConcurrencyToken. /// [ConcurrencyCheck] [Required] public uint RowVersion { get; set; } + partial void Init(); + + /// public void OnSavingChanges() { RowVersion++; } } } - diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 8eae366ba..b2a3f7eb3 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -17,14 +17,10 @@ - + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs index 531b529dc..0b398b60c 100644 --- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -50,31 +50,31 @@ namespace Jellyfin.Server.Implementations.Activity /// public QueryResult GetPagedResult( - Func, IEnumerable> func, + Func, IQueryable> func, int? startIndex, int? limit) { using var dbContext = _provider.CreateContext(); - var result = func.Invoke(dbContext.ActivityLogs).AsQueryable(); + var query = func(dbContext.ActivityLogs).OrderByDescending(entry => entry.DateCreated).AsQueryable(); if (startIndex.HasValue) { - result = result.Where(entry => entry.Id >= startIndex.Value); + query = query.Skip(startIndex.Value); } if (limit.HasValue) { - result = result.OrderByDescending(entry => entry.DateCreated).Take(limit.Value); + query = query.Take(limit.Value); } // This converts the objects from the new database model to the old for compatibility with the existing API. - var list = result.Select(entry => ConvertToOldModel(entry)).ToList(); + var list = query.AsEnumerable().Select(ConvertToOldModel).ToList(); - return new QueryResult() + return new QueryResult { Items = list, - TotalRecordCount = list.Count + TotalRecordCount = dbContext.ActivityLogs.Count() }; } diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index a31f28f64..149ca5020 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -25,6 +25,14 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index 6fc8d251b..23714b24a 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -110,7 +110,7 @@ namespace Jellyfin.Server.Implementations foreach (var entity in ChangeTracker.Entries().Where(e => e.State == EntityState.Modified)) { var saveEntity = entity.Entity as ISavingChanges; - saveEntity.OnSavingChanges(); + saveEntity?.OnSavingChanges(); } return base.SaveChanges(); diff --git a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs index 8fdeab088..eab531d38 100644 --- a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs +++ b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs @@ -27,7 +27,7 @@ namespace Jellyfin.Server.Implementations /// The newly created context. public JellyfinDb CreateContext() { - return _serviceProvider.GetService(); + return _serviceProvider.GetRequiredService(); } } } diff --git a/Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.Designer.cs deleted file mode 100644 index e1ee9b34a..000000000 --- a/Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.Designer.cs +++ /dev/null @@ -1,73 +0,0 @@ -#pragma warning disable CS1591 -#pragma warning disable SA1601 - -// -using System; -using Jellyfin.Server.Implementations; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace Jellyfin.Server.Implementations.Migrations -{ - [DbContext(typeof(JellyfinDb))] - [Migration("20200502231229_InitialSchema")] - partial class InitialSchema - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasDefaultSchema("jellyfin") - .HasAnnotation("ProductVersion", "3.1.3"); - - modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DateCreated") - .HasColumnType("TEXT"); - - b.Property("ItemId") - .HasColumnType("TEXT") - .HasMaxLength(256); - - b.Property("LogSeverity") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(512); - - b.Property("Overview") - .HasColumnType("TEXT") - .HasMaxLength(512); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ShortOverview") - .HasColumnType("TEXT") - .HasMaxLength(512); - - b.Property("Type") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(256); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ActivityLog"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.cs b/Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.cs deleted file mode 100644 index 42fac865c..000000000 --- a/Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.cs +++ /dev/null @@ -1,46 +0,0 @@ -#pragma warning disable CS1591 -#pragma warning disable SA1601 - -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Jellyfin.Server.Implementations.Migrations -{ - public partial class InitialSchema : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.EnsureSchema( - name: "jellyfin"); - - migrationBuilder.CreateTable( - name: "ActivityLog", - schema: "jellyfin", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(maxLength: 512, nullable: false), - Overview = table.Column(maxLength: 512, nullable: true), - ShortOverview = table.Column(maxLength: 512, nullable: true), - Type = table.Column(maxLength: 256, nullable: false), - UserId = table.Column(nullable: false), - ItemId = table.Column(maxLength: 256, nullable: true), - DateCreated = table.Column(nullable: false), - LogSeverity = table.Column(nullable: false), - RowVersion = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ActivityLog", x => x.Id); - }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "ActivityLog", - schema: "jellyfin"); - } - } -} diff --git a/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs new file mode 100644 index 000000000..98a83b745 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs @@ -0,0 +1,72 @@ +#pragma warning disable CS1591 + +// +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDb))] + [Migration("20200514181226_AddActivityLog")] + partial class AddActivityLog + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("jellyfin") + .HasAnnotation("ProductVersion", "3.1.3"); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("Overview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ActivityLogs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.cs b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.cs new file mode 100644 index 000000000..5e0b454d8 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.cs @@ -0,0 +1,46 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1601 + +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Jellyfin.Server.Implementations.Migrations +{ + public partial class AddActivityLog : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "jellyfin"); + + migrationBuilder.CreateTable( + name: "ActivityLogs", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(maxLength: 512, nullable: false), + Overview = table.Column(maxLength: 512, nullable: true), + ShortOverview = table.Column(maxLength: 512, nullable: true), + Type = table.Column(maxLength: 256, nullable: false), + UserId = table.Column(nullable: false), + ItemId = table.Column(maxLength: 256, nullable: true), + DateCreated = table.Column(nullable: false), + LogSeverity = table.Column(nullable: false), + RowVersion = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ActivityLogs", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ActivityLogs", + schema: "jellyfin"); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs index 23a0fdc78..a1b58eb5a 100644 --- a/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs +++ b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs @@ -1,6 +1,3 @@ -#pragma warning disable CS1591 -#pragma warning disable SA1601 - using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; @@ -15,7 +12,9 @@ namespace Jellyfin.Server.Implementations.Migrations public JellyfinDb CreateDbContext(string[] args) { var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseSqlite("Data Source=jellyfin.db"); + optionsBuilder.UseSqlite( + "Data Source=jellyfin.db", + opt => opt.MigrationsAssembly("Jellyfin.Migrations")); return new JellyfinDb(optionsBuilder.Options); } diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index 27f5fe24b..1e7ffd235 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -1,9 +1,7 @@ // using System; -using Jellyfin.Server.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Jellyfin.Server.Implementations.Migrations { @@ -60,7 +58,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasKey("Id"); - b.ToTable("ActivityLog"); + b.ToTable("ActivityLogs"); }); #pragma warning restore 612, 618 } diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index f678e714c..331a32c73 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -1,12 +1,17 @@ using System; using System.Collections.Generic; +using System.IO; using System.Reflection; using Emby.Drawing; using Emby.Server.Implementations; using Jellyfin.Drawing.Skia; +using Jellyfin.Server.Implementations; +using Jellyfin.Server.Implementations.Activity; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Drawing; +using MediaBrowser.Model.Activity; using MediaBrowser.Model.IO; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -56,6 +61,15 @@ namespace Jellyfin.Server Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}."); } + // TODO: Set up scoping and use AddDbContextPool + serviceCollection.AddDbContext( + options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"), + ServiceLifetime.Transient); + + serviceCollection.AddSingleton(); + + serviceCollection.AddSingleton(); + base.RegisterServices(serviceCollection); } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 4194070aa..9eec6ed4e 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -13,9 +13,6 @@ true true enable - - - True @@ -44,10 +41,6 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - @@ -67,6 +60,7 @@ + diff --git a/MediaBrowser.Api/System/ActivityLogService.cs b/MediaBrowser.Api/System/ActivityLogService.cs index f2c37d711..a6bacad4f 100644 --- a/MediaBrowser.Api/System/ActivityLogService.cs +++ b/MediaBrowser.Api/System/ActivityLogService.cs @@ -1,3 +1,7 @@ +using System; +using System.Globalization; +using System.Linq; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Activity; @@ -47,7 +51,14 @@ namespace MediaBrowser.Api.System public object Get(GetActivityLogs request) { - var result = _activityManager.GetPagedResult(request.StartIndex, request.Limit); + DateTime? minDate = string.IsNullOrWhiteSpace(request.MinDate) ? + (DateTime?)null : + DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); + + var filterFunc = new Func, IQueryable>( + entries => entries.Where(entry => entry.DateCreated >= minDate)); + + var result = _activityManager.GetPagedResult(filterFunc, request.StartIndex, request.Limit); return ToOptimizedResult(result); } diff --git a/MediaBrowser.Model/Activity/IActivityManager.cs b/MediaBrowser.Model/Activity/IActivityManager.cs index 6742dc8fc..9dab5e77b 100644 --- a/MediaBrowser.Model/Activity/IActivityManager.cs +++ b/MediaBrowser.Model/Activity/IActivityManager.cs @@ -1,7 +1,6 @@ #pragma warning disable CS1591 using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Jellyfin.Data.Entities; @@ -21,7 +20,7 @@ namespace MediaBrowser.Model.Activity QueryResult GetPagedResult(int? startIndex, int? limit); QueryResult GetPagedResult( - Func, IEnumerable> func, + Func, IQueryable> func, int? startIndex, int? limit); } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 22a42322a..1f5981f10 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -79,8 +79,6 @@ namespace MediaBrowser.Model.Configuration public bool EnableRemoteAccess { get; set; } - public bool CameraUploadUpgraded { get; set; } - public bool CollectionsUpgraded { get; set; } /// diff --git a/MediaBrowser.Model/Devices/DeviceOptions.cs b/MediaBrowser.Model/Devices/DeviceOptions.cs new file mode 100644 index 000000000..8b77fd7fc --- /dev/null +++ b/MediaBrowser.Model/Devices/DeviceOptions.cs @@ -0,0 +1,9 @@ +#pragma warning disable CS1591 + +namespace MediaBrowser.Model.Devices +{ + public class DeviceOptions + { + public string CustomName { get; set; } + } +} diff --git a/MediaBrowser.Model/Devices/DevicesOptions.cs b/MediaBrowser.Model/Devices/DevicesOptions.cs deleted file mode 100644 index 02570650e..000000000 --- a/MediaBrowser.Model/Devices/DevicesOptions.cs +++ /dev/null @@ -1,23 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace MediaBrowser.Model.Devices -{ - public class DevicesOptions - { - public string[] EnabledCameraUploadDevices { get; set; } - public string CameraUploadPath { get; set; } - public bool EnableCameraUploadSubfolders { get; set; } - - public DevicesOptions() - { - EnabledCameraUploadDevices = Array.Empty(); - } - } - - public class DeviceOptions - { - public string CustomName { get; set; } - } -} -- cgit v1.2.3 From 953777f1ba4858f5186086e97910fcb88bfe3d61 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Thu, 14 May 2020 18:12:51 -0400 Subject: Removed unnecessary usings --- Emby.Server.Implementations/ApplicationHost.cs | 2 -- 1 file changed, 2 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 272a9355a..e6410f857 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -80,7 +80,6 @@ using MediaBrowser.Controller.Subtitles; using MediaBrowser.Controller.TV; using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.MediaEncoding.BdInfo; -using MediaBrowser.Model.Activity; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Dlna; @@ -99,7 +98,6 @@ using MediaBrowser.Providers.Subtitles; using MediaBrowser.WebDashboard.Api; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Prometheus.DotNetRuntime; -- cgit v1.2.3 From 3eeb6576d8425c8d2917f861b466dfa36e3994df Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 15 May 2020 17:24:01 -0400 Subject: Migrate User DB to EF Core --- Emby.Server.Implementations/ApplicationHost.cs | 1 - .../Data/SqliteDisplayPreferencesRepository.cs | 225 ++++++ .../Session/SessionManager.cs | 35 +- Jellyfin.Data/Entities/AccessSchedule.cs | 18 +- Jellyfin.Data/Entities/Group.cs | 41 +- Jellyfin.Data/Entities/Permission.cs | 53 +- Jellyfin.Data/Entities/Preference.cs | 81 +-- Jellyfin.Data/Entities/User.cs | 141 ++-- Jellyfin.Data/IHasPermissions.cs | 10 + Jellyfin.Server.Implementations/JellyfinDb.cs | 10 +- .../JellyfinDbProvider.cs | 2 +- .../20200504195702_UserSchema.Designer.cs | 324 --------- .../Migrations/20200504195702_UserSchema.cs | 219 ------ .../Migrations/20200517002411_AddUsers.Designer.cs | 404 +++++++++++ .../Migrations/20200517002411_AddUsers.cs | 297 ++++++++ .../Migrations/JellyfinDbModelSnapshot.cs | 186 +++-- .../User/DefaultAuthenticationProvider.cs | 185 ----- .../User/DefaultPasswordResetProvider.cs | 126 ---- .../User/DeviceAccessEntryPoint.cs | 65 -- .../User/InvalidAuthProvider.cs | 47 -- .../User/UserManager.cs | 771 --------------------- .../Users/DefaultAuthenticationProvider.cs | 185 +++++ .../Users/DefaultPasswordResetProvider.cs | 127 ++++ .../Users/DeviceAccessEntryPoint.cs | 66 ++ .../Users/InvalidAuthProvider.cs | 47 ++ .../Users/UserManager.cs | 763 ++++++++++++++++++++ Jellyfin.Server/CoreAppHost.cs | 6 + Jellyfin.Server/Jellyfin.Server.csproj | 1 - .../Migrations/Routines/MigrateUserDb.cs | 100 ++- MediaBrowser.Model/Users/UserPolicy.cs | 8 +- 30 files changed, 2526 insertions(+), 2018 deletions(-) create mode 100644 Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs create mode 100644 Jellyfin.Data/IHasPermissions.cs delete mode 100644 Jellyfin.Server.Implementations/Migrations/20200504195702_UserSchema.Designer.cs delete mode 100644 Jellyfin.Server.Implementations/Migrations/20200504195702_UserSchema.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20200517002411_AddUsers.Designer.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20200517002411_AddUsers.cs delete mode 100644 Jellyfin.Server.Implementations/User/DefaultAuthenticationProvider.cs delete mode 100644 Jellyfin.Server.Implementations/User/DefaultPasswordResetProvider.cs delete mode 100644 Jellyfin.Server.Implementations/User/DeviceAccessEntryPoint.cs delete mode 100644 Jellyfin.Server.Implementations/User/InvalidAuthProvider.cs delete mode 100644 Jellyfin.Server.Implementations/User/UserManager.cs create mode 100644 Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs create mode 100644 Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs create mode 100644 Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs create mode 100644 Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs create mode 100644 Jellyfin.Server.Implementations/Users/UserManager.cs (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index b2fe6a3ab..56b103763 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -562,7 +562,6 @@ namespace Emby.Server.Implementations // TODO: Refactor to eliminate the circular dependency here so that Lazy isn't required serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); - serviceCollection.AddSingleton(); // TODO: Refactor to eliminate the circular dependency here so that Lazy isn't required // TODO: Add StartupOptions.FFmpegPath to IConfiguration and remove this custom activation diff --git a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs new file mode 100644 index 000000000..63d0321b7 --- /dev/null +++ b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs @@ -0,0 +1,225 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text.Json; +using System.Threading; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Json; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; +using SQLitePCL.pretty; + +namespace Emby.Server.Implementations.Data +{ + /// + /// Class SQLiteDisplayPreferencesRepository. + /// + public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository + { + private readonly IFileSystem _fileSystem; + + private readonly JsonSerializerOptions _jsonOptions; + + public SqliteDisplayPreferencesRepository(ILogger logger, IApplicationPaths appPaths, IFileSystem fileSystem) + : base(logger) + { + _fileSystem = fileSystem; + + _jsonOptions = JsonDefaults.GetOptions(); + + DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db"); + } + + /// + /// Gets the name of the repository. + /// + /// The name. + public string Name => "SQLite"; + + public void Initialize() + { + try + { + InitializeInternal(); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error loading database file. Will reset and retry."); + + _fileSystem.DeleteFile(DbFilePath); + + InitializeInternal(); + } + } + + /// + /// Opens the connection to the database + /// + /// Task. + private void InitializeInternal() + { + string[] queries = + { + "create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)", + "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)" + }; + + using (var connection = GetConnection()) + { + connection.RunQueries(queries); + } + } + + /// + /// Save the display preferences associated with an item in the repo + /// + /// The display preferences. + /// The user id. + /// The client. + /// The cancellation token. + /// item + public void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, CancellationToken cancellationToken) + { + if (displayPreferences == null) + { + throw new ArgumentNullException(nameof(displayPreferences)); + } + + if (string.IsNullOrEmpty(displayPreferences.Id)) + { + throw new ArgumentException("Display preferences has an invalid Id", nameof(displayPreferences)); + } + + cancellationToken.ThrowIfCancellationRequested(); + + using (var connection = GetConnection()) + { + connection.RunInTransaction( + db => SaveDisplayPreferences(displayPreferences, userId, client, db), + TransactionMode); + } + } + + private void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, IDatabaseConnection connection) + { + var serialized = JsonSerializer.SerializeToUtf8Bytes(displayPreferences, _jsonOptions); + + using (var statement = connection.PrepareStatement("replace into userdisplaypreferences (id, userid, client, data) values (@id, @userId, @client, @data)")) + { + statement.TryBind("@id", new Guid(displayPreferences.Id).ToByteArray()); + statement.TryBind("@userId", userId.ToByteArray()); + statement.TryBind("@client", client); + statement.TryBind("@data", serialized); + + statement.MoveNext(); + } + } + + /// + /// Save all display preferences associated with a user in the repo + /// + /// The display preferences. + /// The user id. + /// The cancellation token. + /// item + public void SaveAllDisplayPreferences(IEnumerable displayPreferences, Guid userId, CancellationToken cancellationToken) + { + if (displayPreferences == null) + { + throw new ArgumentNullException(nameof(displayPreferences)); + } + + cancellationToken.ThrowIfCancellationRequested(); + + using (var connection = GetConnection()) + { + connection.RunInTransaction( + db => + { + foreach (var displayPreference in displayPreferences) + { + SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db); + } + }, + TransactionMode); + } + } + + /// + /// Gets the display preferences. + /// + /// The display preferences id. + /// The user id. + /// The client. + /// Task{DisplayPreferences}. + /// item + public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, Guid userId, string client) + { + if (string.IsNullOrEmpty(displayPreferencesId)) + { + throw new ArgumentNullException(nameof(displayPreferencesId)); + } + + var guidId = displayPreferencesId.GetMD5(); + + using (var connection = GetConnection(true)) + { + using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where id = @id and userId=@userId and client=@client")) + { + statement.TryBind("@id", guidId.ToByteArray()); + statement.TryBind("@userId", userId.ToByteArray()); + statement.TryBind("@client", client); + + foreach (var row in statement.ExecuteQuery()) + { + return Get(row); + } + } + } + + return new DisplayPreferences + { + Id = guidId.ToString("N", CultureInfo.InvariantCulture) + }; + } + + /// + /// Gets all display preferences for the given user. + /// + /// The user id. + /// Task{DisplayPreferences}. + /// item + public IEnumerable GetAllDisplayPreferences(Guid userId) + { + var list = new List(); + + using (var connection = GetConnection(true)) + using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId")) + { + statement.TryBind("@userId", userId.ToByteArray()); + + foreach (var row in statement.ExecuteQuery()) + { + list.Add(Get(row)); + } + } + + return list; + } + + private DisplayPreferences Get(IReadOnlyList row) + => JsonSerializer.Deserialize(row[0].ToBlob(), _jsonOptions); + + public void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken) + => SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken); + + public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client) + => GetDisplayPreferences(displayPreferencesId, new Guid(userId), client); + } +} diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 84b16f1c7..54c26a8a5 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; @@ -14,7 +15,6 @@ using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; @@ -27,6 +27,7 @@ using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; +using Episode = MediaBrowser.Controller.Entities.TV.Episode; namespace Emby.Server.Implementations.Session { @@ -254,7 +255,7 @@ namespace Emby.Server.Implementations.Session string deviceId, string deviceName, string remoteEndPoint, - Jellyfin.Data.Entities.User user) + User user) { CheckDisposed(); @@ -438,7 +439,7 @@ namespace Emby.Server.Implementations.Session string deviceId, string deviceName, string remoteEndPoint, - Jellyfin.Data.Entities.User user) + User user) { CheckDisposed(); @@ -457,7 +458,7 @@ namespace Emby.Server.Implementations.Session sessionInfo.UserId = user?.Id ?? Guid.Empty; sessionInfo.UserName = user?.Username; - sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user); + sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user); sessionInfo.RemoteEndPoint = remoteEndPoint; sessionInfo.Client = appName; @@ -483,7 +484,7 @@ namespace Emby.Server.Implementations.Session string deviceId, string deviceName, string remoteEndPoint, - Jellyfin.Data.Entities.User user) + User user) { var sessionInfo = new SessionInfo(this, _logger) { @@ -497,7 +498,7 @@ namespace Emby.Server.Implementations.Session sessionInfo.UserId = user?.Id ?? Guid.Empty; sessionInfo.UserName = username; - sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user); + sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user); sessionInfo.RemoteEndPoint = remoteEndPoint; if (string.IsNullOrEmpty(deviceName)) @@ -520,9 +521,9 @@ namespace Emby.Server.Implementations.Session return sessionInfo; } - private List GetUsers(SessionInfo session) + private List GetUsers(SessionInfo session) { - var users = new List(); + var users = new List(); if (session.UserId != Guid.Empty) { @@ -680,7 +681,7 @@ namespace Emby.Server.Implementations.Session /// /// The user object. /// The item. - private void OnPlaybackStart(Jellyfin.Data.Entities.User user, BaseItem item) + private void OnPlaybackStart(User user, BaseItem item) { var data = _userDataManager.GetUserData(user, item); @@ -763,7 +764,7 @@ namespace Emby.Server.Implementations.Session StartIdleCheckTimer(); } - private void OnPlaybackProgress(Jellyfin.Data.Entities.User user, BaseItem item, PlaybackProgressInfo info) + private void OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info) { var data = _userDataManager.GetUserData(user, item); @@ -789,7 +790,7 @@ namespace Emby.Server.Implementations.Session } } - private static bool UpdatePlaybackSettings(Jellyfin.Data.Entities.User user, PlaybackProgressInfo info, UserItemData data) + private static bool UpdatePlaybackSettings(User user, PlaybackProgressInfo info, UserItemData data) { var changed = false; @@ -949,7 +950,7 @@ namespace Emby.Server.Implementations.Session _logger); } - private bool OnPlaybackStopped(Jellyfin.Data.Entities.User user, BaseItem item, long? positionTicks, bool playbackFailed) + private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed) { bool playedToCompletion = false; @@ -1163,7 +1164,7 @@ namespace Emby.Server.Implementations.Session await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false); } - private IEnumerable TranslateItemForPlayback(Guid id, Jellyfin.Data.Entities.User user) + private IEnumerable TranslateItemForPlayback(Guid id, User user) { var item = _libraryManager.GetItemById(id); @@ -1216,7 +1217,7 @@ namespace Emby.Server.Implementations.Session return new[] { item }; } - private IEnumerable TranslateItemForInstantMix(Guid id, Jellyfin.Data.Entities.User user) + private IEnumerable TranslateItemForInstantMix(Guid id, User user) { var item = _libraryManager.GetItemById(id); @@ -1399,7 +1400,7 @@ namespace Emby.Server.Implementations.Session { CheckDisposed(); - Jellyfin.Data.Entities.User user = null; + User user = null; if (request.UserId != Guid.Empty) { user = _userManager.GetUserById(request.UserId); @@ -1455,7 +1456,7 @@ namespace Emby.Server.Implementations.Session return returnResult; } - private string GetAuthorizationToken(Jellyfin.Data.Entities.User user, string deviceId, string app, string appVersion, string deviceName) + private string GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName) { var existing = _authRepo.Get( new AuthenticationInfoQuery @@ -1701,7 +1702,7 @@ namespace Emby.Server.Implementations.Session return info; } - private string GetImageCacheTag(Jellyfin.Data.Entities.User user) + private string GetImageCacheTag(User user) { try { diff --git a/Jellyfin.Data/Entities/AccessSchedule.cs b/Jellyfin.Data/Entities/AccessSchedule.cs index 7966cdb50..711e94dd1 100644 --- a/Jellyfin.Data/Entities/AccessSchedule.cs +++ b/Jellyfin.Data/Entities/AccessSchedule.cs @@ -1,5 +1,7 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; using Jellyfin.Data.Enums; namespace Jellyfin.Data.Entities @@ -20,8 +22,9 @@ namespace Jellyfin.Data.Entities /// The day of the week. /// The start hour. /// The end hour. - public AccessSchedule(DynamicDayOfWeek dayOfWeek, double startHour, double endHour) + public AccessSchedule(DynamicDayOfWeek dayOfWeek, double startHour, double endHour, Guid userId) { + UserId = userId; DayOfWeek = dayOfWeek; StartHour = startHour; EndHour = endHour; @@ -34,15 +37,20 @@ namespace Jellyfin.Data.Entities /// The start hour. /// The end hour. /// The newly created instance. - public static AccessSchedule CreateInstance(DynamicDayOfWeek dayOfWeek, double startHour, double endHour) + public static AccessSchedule CreateInstance(DynamicDayOfWeek dayOfWeek, double startHour, double endHour, Guid userId) { - return new AccessSchedule(dayOfWeek, startHour, endHour); + return new AccessSchedule(dayOfWeek, startHour, endHour, userId); } + [JsonIgnore] [Key] [Required] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; set; } + + [Required] + [ForeignKey("Id")] + public Guid UserId { get; set; } /// /// Gets or sets the day of week. diff --git a/Jellyfin.Data/Entities/Group.cs b/Jellyfin.Data/Entities/Group.cs index 54f9f4905..017fb2234 100644 --- a/Jellyfin.Data/Entities/Group.cs +++ b/Jellyfin.Data/Entities/Group.cs @@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations.Schema; namespace Jellyfin.Data.Entities { - public partial class Group + public partial class Group : IHasPermissions, ISavingChanges { partial void Init(); @@ -14,35 +14,29 @@ namespace Jellyfin.Data.Entities /// protected Group() { - GroupPermissions = new HashSet(); + Permissions = new HashSet(); ProviderMappings = new HashSet(); Preferences = new HashSet(); Init(); } - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static Group CreateGroupUnsafe() - { - return new Group(); - } - /// /// Public constructor with required data /// /// - /// - public Group(string name, User _user0) + /// + public Group(string name, User user) { - if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); - this.Name = name; + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name)); + } - if (_user0 == null) throw new ArgumentNullException(nameof(_user0)); - _user0.Groups.Add(this); + this.Name = name; + user.Groups.Add(this); - this.GroupPermissions = new HashSet(); + this.Permissions = new HashSet(); this.ProviderMappings = new HashSet(); this.Preferences = new HashSet(); @@ -54,9 +48,9 @@ namespace Jellyfin.Data.Entities /// /// /// - public static Group Create(string name, User _user0) + public static Group Create(string name, User user) { - return new Group(name, _user0); + return new Group(name, user); } /************************************************************************* @@ -68,8 +62,7 @@ namespace Jellyfin.Data.Entities /// [Key] [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public Guid Id { get; protected set; } /// /// Required, Max length = 255 @@ -96,13 +89,13 @@ namespace Jellyfin.Data.Entities *************************************************************************/ [ForeignKey("Permission_GroupPermissions_Id")] - public virtual ICollection GroupPermissions { get; protected set; } + public ICollection Permissions { get; protected set; } [ForeignKey("ProviderMapping_ProviderMappings_Id")] - public virtual ICollection ProviderMappings { get; protected set; } + public ICollection ProviderMappings { get; protected set; } [ForeignKey("Preference_Preferences_Id")] - public virtual ICollection Preferences { get; protected set; } + public ICollection Preferences { get; protected set; } } } diff --git a/Jellyfin.Data/Entities/Permission.cs b/Jellyfin.Data/Entities/Permission.cs index 0b5b52cbd..136e7abd3 100644 --- a/Jellyfin.Data/Entities/Permission.cs +++ b/Jellyfin.Data/Entities/Permission.cs @@ -3,10 +3,11 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Runtime.CompilerServices; +using Jellyfin.Data.Enums; namespace Jellyfin.Data.Entities { - public partial class Permission + public partial class Permission : ISavingChanges { partial void Init(); @@ -18,33 +19,16 @@ namespace Jellyfin.Data.Entities Init(); } - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static Permission CreatePermissionUnsafe() - { - return new Permission(); - } - /// /// Public constructor with required data /// /// /// - /// - /// - public Permission(Enums.PermissionKind kind, bool value, User _user0, Group _group1) + /// + public Permission(PermissionKind kind, bool value) { - this.Kind = kind; - - this.Value = value; - - if (_user0 == null) throw new ArgumentNullException(nameof(_user0)); - _user0.Permissions.Add(this); - - if (_group1 == null) throw new ArgumentNullException(nameof(_group1)); - _group1.GroupPermissions.Add(this); - + Kind = kind; + Value = value; Init(); } @@ -54,11 +38,10 @@ namespace Jellyfin.Data.Entities /// /// /// - /// - /// - public static Permission Create(Enums.PermissionKind kind, bool value, User _user0, Group _group1) + /// + public static Permission Create(PermissionKind kind, bool value) { - return new Permission(kind, value, _user0, _group1); + return new Permission(kind, value); } /************************************************************************* @@ -76,31 +59,32 @@ namespace Jellyfin.Data.Entities /// /// Backing field for Kind /// - protected Enums.PermissionKind _Kind; + protected PermissionKind _Kind; /// /// When provided in a partial class, allows value of Kind to be changed before setting. /// - partial void SetKind(Enums.PermissionKind oldValue, ref Enums.PermissionKind newValue); + partial void SetKind(PermissionKind oldValue, ref PermissionKind newValue); /// /// When provided in a partial class, allows value of Kind to be changed before returning. /// - partial void GetKind(ref Enums.PermissionKind result); + partial void GetKind(ref PermissionKind result); /// /// Required /// [Required] - public Enums.PermissionKind Kind + public PermissionKind Kind { get { - Enums.PermissionKind value = _Kind; + PermissionKind value = _Kind; GetKind(ref value); - return (_Kind = value); + return _Kind = value; } + set { - Enums.PermissionKind oldValue = _Kind; + PermissionKind oldValue = _Kind; SetKind(oldValue, ref value); if (oldValue != value) { @@ -117,7 +101,7 @@ namespace Jellyfin.Data.Entities public bool Value { get; set; } /// - /// Required, ConcurrenyToken + /// Required, ConcurrencyToken. /// [ConcurrencyCheck] [Required] @@ -138,7 +122,6 @@ namespace Jellyfin.Data.Entities { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } - } } diff --git a/Jellyfin.Data/Entities/Preference.cs b/Jellyfin.Data/Entities/Preference.cs index 505f52e6b..56a07d440 100644 --- a/Jellyfin.Data/Entities/Preference.cs +++ b/Jellyfin.Data/Entities/Preference.cs @@ -1,63 +1,33 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Enums; namespace Jellyfin.Data.Entities { - public partial class Preference + /// + /// An entity representing a preference attached to a user or group. + /// + public class Preference : ISavingChanges { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected Preference() - { - Init(); - } - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// Initializes a new instance of the class. + /// Public constructor with required data. /// - public static Preference CreatePreferenceUnsafe() + /// The preference kind. + /// The value. + public Preference(PreferenceKind kind, string value) { - return new Preference(); + Kind = kind; + Value = value ?? throw new ArgumentNullException(nameof(value)); } /// - /// Public constructor with required data - /// - /// - /// - /// - /// - public Preference(Enums.PreferenceKind kind, string value, User _user0, Group _group1) - { - this.Kind = kind; - - if (string.IsNullOrEmpty(value)) throw new ArgumentNullException(nameof(value)); - this.Value = value; - - if (_user0 == null) throw new ArgumentNullException(nameof(_user0)); - _user0.Preferences.Add(this); - - if (_group1 == null) throw new ArgumentNullException(nameof(_group1)); - _group1.Preferences.Add(this); - - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) + /// Initializes a new instance of the class. + /// Default constructor. Protected due to required properties, but present because EF needs it. /// - /// - /// - /// - /// - public static Preference Create(Enums.PreferenceKind kind, string value, User _user0, Group _group1) + protected Preference() { - return new Preference(kind, value, _user0, _group1); } /************************************************************************* @@ -76,7 +46,7 @@ namespace Jellyfin.Data.Entities /// Required /// [Required] - public Enums.PreferenceKind Kind { get; set; } + public PreferenceKind Kind { get; set; } /// /// Required, Max length = 65535 @@ -87,21 +57,28 @@ namespace Jellyfin.Data.Entities public string Value { get; set; } /// - /// Required, ConcurrenyToken + /// Required, ConcurrencyToken. /// [ConcurrencyCheck] [Required] public uint RowVersion { get; set; } + /// + /// Static create function (for use in LINQ queries, etc.) + /// + /// The preference kind. + /// The value. + /// The new instance. + public static Preference Create(PreferenceKind kind, string value) + { + return new Preference(kind, value); + } + + /// public void OnSavingChanges() { RowVersion++; } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } } diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs index 17913959e..7252ef230 100644 --- a/Jellyfin.Data/Entities/User.cs +++ b/Jellyfin.Data/Entities/User.cs @@ -9,45 +9,23 @@ using Jellyfin.Data.Enums; namespace Jellyfin.Data.Entities { - public partial class User + /// + /// An entity representing a user. + /// + public partial class User : IHasPermissions, ISavingChanges { /// /// The values being delimited here are Guids, so commas work as they do not appear in Guids. /// private const char Delimiter = ','; - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected User() - { - Groups = new HashSet(); - Permissions = new HashSet(); - ProviderMappings = new HashSet(); - Preferences = new HashSet(); - AccessSchedules = new HashSet(); - - Init(); - } - /// - /// Public constructor with required data + /// Initializes a new instance of the class. + /// Public constructor with required data. /// - /// - /// - /// - /// - /// - /// - public User( - string username, - bool mustUpdatePassword, - string authenticationProviderId, - int invalidLoginAttemptCount, - SubtitlePlaybackMode subtitleMode, - bool playDefaultAudioTrack) + /// The username for the new user. + /// The authentication provider's Id + public User(string username, string authenticationProviderId) { if (string.IsNullOrEmpty(username)) { @@ -60,11 +38,7 @@ namespace Jellyfin.Data.Entities } Username = username; - MustUpdatePassword = mustUpdatePassword; AuthenticationProviderId = authenticationProviderId; - InvalidLoginAttemptCount = invalidLoginAttemptCount; - SubtitleMode = subtitleMode; - PlayDefaultAudioTrack = playDefaultAudioTrack; Groups = new HashSet(); Permissions = new HashSet(); @@ -74,6 +48,8 @@ namespace Jellyfin.Data.Entities // Set default values Id = Guid.NewGuid(); + InvalidLoginAttemptCount = 0; + MustUpdatePassword = false; DisplayMissingEpisodes = false; DisplayCollectionsView = false; HidePlayedInLatest = true; @@ -81,36 +57,40 @@ namespace Jellyfin.Data.Entities RememberSubtitleSelections = true; EnableNextEpisodeAutoPlay = true; EnableAutoLogin = false; + PlayDefaultAudioTrack = true; + SubtitleMode = SubtitlePlaybackMode.Default; + AddDefaultPermissions(); + AddDefaultPreferences(); Init(); } /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// Initializes a new instance of the class. + /// Default constructor. Protected due to required properties, but present because EF needs it. /// - public static User CreateUserUnsafe() + protected User() { - return new User(); + Groups = new HashSet(); + Permissions = new HashSet(); + ProviderMappings = new HashSet(); + Preferences = new HashSet(); + AccessSchedules = new HashSet(); + + AddDefaultPermissions(); + AddDefaultPreferences(); + Init(); } /// /// Static create function (for use in LINQ queries, etc.) /// - /// - /// - /// - /// - /// - /// - public static User Create( - string username, - bool mustUpdatePassword, - string authenticationProviderId, - int invalidLoginAttemptCount, - SubtitlePlaybackMode subtitleMode, - bool playDefaultAudioTrack) + /// The username for the created user. + /// The Id of the user's authentication provider. + /// The created instance. + public static User Create(string username, string authenticationProviderId) { - return new User(username, mustUpdatePassword, authenticationProviderId, invalidLoginAttemptCount, subtitleMode, playDefaultAudioTrack); + return new User(username, authenticationProviderId); } /************************************************************************* @@ -131,7 +111,6 @@ namespace Jellyfin.Data.Entities [Required] [MaxLength(255)] [StringLength(255)] - [JsonPropertyName("Name")] public string Username { get; set; } /// @@ -199,6 +178,7 @@ namespace Jellyfin.Data.Entities public bool PlayDefaultAudioTrack { get; set; } /// + /// Gets or sets the subtitle language preference. /// Max length = 255 /// [MaxLength(255)] @@ -237,6 +217,7 @@ namespace Jellyfin.Data.Entities public int? RemoteClientBitrateLimit { get; set; } /// + /// Gets or sets the internal id. /// This is a temporary stopgap for until the library db is migrated. /// This corresponds to the value of the index of this user in the library db. /// @@ -246,7 +227,8 @@ namespace Jellyfin.Data.Entities public ImageInfo ProfileImage { get; set; } /// - /// Required, ConcurrenyToken + /// Gets or sets the row version. + /// Required, ConcurrenyToken. /// [ConcurrencyCheck] [Required] @@ -260,23 +242,25 @@ namespace Jellyfin.Data.Entities /************************************************************************* * Navigation properties *************************************************************************/ - [ForeignKey("Group_Groups_Id")] + [ForeignKey("Group_Groups_Guid")] public ICollection Groups { get; protected set; } - [ForeignKey("Permission_Permissions_Id")] + [ForeignKey("Permission_Permissions_Guid")] public ICollection Permissions { get; protected set; } [ForeignKey("ProviderMapping_ProviderMappings_Id")] public ICollection ProviderMappings { get; protected set; } - [ForeignKey("Preference_Preferences_Id")] + [ForeignKey("Preference_Preferences_Guid")] public ICollection Preferences { get; protected set; } public ICollection AccessSchedules { get; protected set; } + partial void Init(); + public bool HasPermission(PermissionKind permission) { - return Permissions.Select(p => p.Kind).Contains(permission); + return Permissions.First(p => p.Kind == permission).Value; } public void SetPermission(PermissionKind kind, bool value) @@ -287,11 +271,12 @@ namespace Jellyfin.Data.Entities public string[] GetPreference(PreferenceKind preference) { - return Preferences + var val = Preferences .Where(p => p.Kind == preference) .Select(p => p.Value) - .First() - .Split(Delimiter); + .First(); + + return Equals(val, string.Empty) ? Array.Empty() : val.Split(Delimiter); } public void SetPreference(PreferenceKind preference, string[] values) @@ -332,5 +317,39 @@ namespace Jellyfin.Data.Entities return hour >= schedule.StartHour && hour <= schedule.EndHour; } + + // TODO: make these user configurable? + private void AddDefaultPermissions() + { + Permissions.Add(new Permission(PermissionKind.IsAdministrator, false)); + Permissions.Add(new Permission(PermissionKind.IsDisabled, false)); + Permissions.Add(new Permission(PermissionKind.IsHidden, false)); + Permissions.Add(new Permission(PermissionKind.EnableAllChannels, false)); + Permissions.Add(new Permission(PermissionKind.EnableAllDevices, true)); + Permissions.Add(new Permission(PermissionKind.EnableAllFolders, false)); + Permissions.Add(new Permission(PermissionKind.EnableContentDeletion, false)); + Permissions.Add(new Permission(PermissionKind.EnableContentDownloading, true)); + Permissions.Add(new Permission(PermissionKind.EnableMediaConversion, true)); + Permissions.Add(new Permission(PermissionKind.EnableMediaPlayback, true)); + Permissions.Add(new Permission(PermissionKind.EnablePlaybackRemuxing, true)); + Permissions.Add(new Permission(PermissionKind.EnablePublicSharing, true)); + Permissions.Add(new Permission(PermissionKind.EnableRemoteAccess, true)); + Permissions.Add(new Permission(PermissionKind.EnableSyncTranscoding, true)); + Permissions.Add(new Permission(PermissionKind.EnableAudioPlaybackTranscoding, true)); + Permissions.Add(new Permission(PermissionKind.EnableLiveTvAccess, true)); + Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, true)); + Permissions.Add(new Permission(PermissionKind.EnableSharedDeviceControl, true)); + Permissions.Add(new Permission(PermissionKind.EnableVideoPlaybackTranscoding, true)); + Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false)); + Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false)); + } + + private void AddDefaultPreferences() + { + foreach (var val in Enum.GetValues(typeof(PreferenceKind)).Cast()) + { + Preferences.Add(new Preference(val, string.Empty)); + } + } } } diff --git a/Jellyfin.Data/IHasPermissions.cs b/Jellyfin.Data/IHasPermissions.cs new file mode 100644 index 000000000..a77e51e1e --- /dev/null +++ b/Jellyfin.Data/IHasPermissions.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Jellyfin.Data.Entities; + +namespace Jellyfin.Data +{ + public interface IHasPermissions + { + ICollection Permissions { get; } + } +} diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index 9ac97a131..8eb35ec87 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -16,6 +16,13 @@ namespace Jellyfin.Server.Implementations public partial class JellyfinDb : DbContext { public virtual DbSet ActivityLogs { get; set; } + + public virtual DbSet Groups { get; set; } + + public virtual DbSet Permissions { get; set; } + + public virtual DbSet Preferences { get; set; } + public virtual DbSet Users { get; set; } /*public virtual DbSet Artwork { get; set; } public virtual DbSet Books { get; set; } @@ -30,7 +37,6 @@ namespace Jellyfin.Server.Implementations public virtual DbSet Episodes { get; set; } public virtual DbSet EpisodeMetadata { get; set; } public virtual DbSet Genres { get; set; } - public virtual DbSet Groups { get; set; } public virtual DbSet Libraries { get; set; } public virtual DbSet LibraryItems { get; set; } public virtual DbSet LibraryRoot { get; set; } @@ -43,12 +49,10 @@ namespace Jellyfin.Server.Implementations public virtual DbSet MovieMetadata { get; set; } public virtual DbSet MusicAlbums { get; set; } public virtual DbSet MusicAlbumMetadata { get; set; } - public virtual DbSet Permissions { get; set; } public virtual DbSet People { get; set; } public virtual DbSet PersonRoles { get; set; } public virtual DbSet Photo { get; set; } public virtual DbSet PhotoMetadata { get; set; } - public virtual DbSet Preferences { get; set; } public virtual DbSet ProviderMappings { get; set; } public virtual DbSet Ratings { get; set; } diff --git a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs index eab531d38..8f5c19900 100644 --- a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs +++ b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs @@ -18,7 +18,7 @@ namespace Jellyfin.Server.Implementations public JellyfinDbProvider(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; - serviceProvider.GetService().Database.Migrate(); + serviceProvider.GetRequiredService().Database.Migrate(); } /// diff --git a/Jellyfin.Server.Implementations/Migrations/20200504195702_UserSchema.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200504195702_UserSchema.Designer.cs deleted file mode 100644 index 8313c6a3b..000000000 --- a/Jellyfin.Server.Implementations/Migrations/20200504195702_UserSchema.Designer.cs +++ /dev/null @@ -1,324 +0,0 @@ -#pragma warning disable CS1591 -#pragma warning disable SA1601 - -// -using System; -using Jellyfin.Server.Implementations; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace Jellyfin.Server.Implementations.Migrations -{ - [DbContext(typeof(JellyfinDb))] - [Migration("20200504195702_UserSchema")] - partial class UserSchema - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasDefaultSchema("jellyfin") - .HasAnnotation("ProductVersion", "3.1.3"); - - modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DateCreated") - .HasColumnType("TEXT"); - - b.Property("ItemId") - .HasColumnType("TEXT") - .HasMaxLength(256); - - b.Property("LogSeverity") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(512); - - b.Property("Overview") - .HasColumnType("TEXT") - .HasMaxLength(512); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ShortOverview") - .HasColumnType("TEXT") - .HasMaxLength(512); - - b.Property("Type") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(256); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ActivityLog"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Group_Groups_Id") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(255); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("Group_Groups_Id"); - - b.ToTable("Group"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("Permission_GroupPermissions_Id") - .HasColumnType("INTEGER"); - - b.Property("Permission_Permissions_Id") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("Permission_GroupPermissions_Id"); - - b.HasIndex("Permission_Permissions_Id"); - - b.ToTable("Permission"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("Preference_Preferences_Id") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(65535); - - b.HasKey("Id"); - - b.HasIndex("Preference_Preferences_Id"); - - b.ToTable("Preference"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ProviderMapping", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ProviderData") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(65535); - - b.Property("ProviderMapping_ProviderMappings_Id") - .HasColumnType("INTEGER"); - - b.Property("ProviderName") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(255); - - b.Property("ProviderSecrets") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(65535); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ProviderMapping_ProviderMappings_Id"); - - b.ToTable("ProviderMapping"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AudioLanguagePreference") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(255); - - b.Property("AuthenticationProviderId") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(255); - - b.Property("DisplayCollectionsView") - .HasColumnType("INTEGER"); - - b.Property("DisplayMissingEpisodes") - .HasColumnType("INTEGER"); - - b.Property("EnableNextEpisodeAutoPlay") - .HasColumnType("INTEGER"); - - b.Property("EnableUserPreferenceAccess") - .HasColumnType("INTEGER"); - - b.Property("GroupedFolders") - .HasColumnType("TEXT") - .HasMaxLength(65535); - - b.Property("HidePlayedInLatest") - .HasColumnType("INTEGER"); - - b.Property("InvalidLoginAttemptCount") - .HasColumnType("INTEGER"); - - b.Property("LatestItemExcludes") - .HasColumnType("TEXT") - .HasMaxLength(65535); - - b.Property("LoginAttemptsBeforeLockout") - .HasColumnType("INTEGER"); - - b.Property("MustUpdatePassword") - .HasColumnType("INTEGER"); - - b.Property("MyMediaExcludes") - .HasColumnType("TEXT") - .HasMaxLength(65535); - - b.Property("OrderedViews") - .HasColumnType("TEXT") - .HasMaxLength(65535); - - b.Property("Password") - .HasColumnType("TEXT") - .HasMaxLength(65535); - - b.Property("PlayDefaultAudioTrack") - .HasColumnType("INTEGER"); - - b.Property("RememberAudioSelections") - .HasColumnType("INTEGER"); - - b.Property("RememberSubtitleSelections") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SubtitleLanguagePrefernce") - .HasColumnType("TEXT") - .HasMaxLength(255); - - b.Property("SubtitleMode") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(255); - - b.Property("Username") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(255); - - b.HasKey("Id"); - - b.ToTable("User"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Group", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("Groups") - .HasForeignKey("Group_Groups_Id"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => - { - b.HasOne("Jellyfin.Data.Entities.Group", null) - .WithMany("GroupPermissions") - .HasForeignKey("Permission_GroupPermissions_Id"); - - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("Permissions") - .HasForeignKey("Permission_Permissions_Id"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => - { - b.HasOne("Jellyfin.Data.Entities.Group", null) - .WithMany("Preferences") - .HasForeignKey("Preference_Preferences_Id"); - - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("Preferences") - .HasForeignKey("Preference_Preferences_Id"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ProviderMapping", b => - { - b.HasOne("Jellyfin.Data.Entities.Group", null) - .WithMany("ProviderMappings") - .HasForeignKey("ProviderMapping_ProviderMappings_Id"); - - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("ProviderMappings") - .HasForeignKey("ProviderMapping_ProviderMappings_Id"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Jellyfin.Server.Implementations/Migrations/20200504195702_UserSchema.cs b/Jellyfin.Server.Implementations/Migrations/20200504195702_UserSchema.cs deleted file mode 100644 index f24ccccbf..000000000 --- a/Jellyfin.Server.Implementations/Migrations/20200504195702_UserSchema.cs +++ /dev/null @@ -1,219 +0,0 @@ -#pragma warning disable CS1591 -#pragma warning disable SA1601 - -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Jellyfin.Server.Implementations.Migrations -{ - public partial class UserSchema : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "User", - schema: "jellyfin", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Username = table.Column(maxLength: 255, nullable: false), - Password = table.Column(maxLength: 65535, nullable: true), - MustUpdatePassword = table.Column(nullable: false), - AudioLanguagePreference = table.Column(maxLength: 255, nullable: false), - AuthenticationProviderId = table.Column(maxLength: 255, nullable: false), - GroupedFolders = table.Column(maxLength: 65535, nullable: true), - InvalidLoginAttemptCount = table.Column(nullable: false), - LatestItemExcludes = table.Column(maxLength: 65535, nullable: true), - LoginAttemptsBeforeLockout = table.Column(nullable: true), - MyMediaExcludes = table.Column(maxLength: 65535, nullable: true), - OrderedViews = table.Column(maxLength: 65535, nullable: true), - SubtitleMode = table.Column(maxLength: 255, nullable: false), - PlayDefaultAudioTrack = table.Column(nullable: false), - SubtitleLanguagePrefernce = table.Column(maxLength: 255, nullable: true), - DisplayMissingEpisodes = table.Column(nullable: true), - DisplayCollectionsView = table.Column(nullable: true), - HidePlayedInLatest = table.Column(nullable: true), - RememberAudioSelections = table.Column(nullable: true), - RememberSubtitleSelections = table.Column(nullable: true), - EnableNextEpisodeAutoPlay = table.Column(nullable: true), - EnableUserPreferenceAccess = table.Column(nullable: true), - RowVersion = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_User", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Group", - schema: "jellyfin", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(maxLength: 255, nullable: false), - RowVersion = table.Column(nullable: false), - Group_Groups_Id = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Group", x => x.Id); - table.ForeignKey( - name: "FK_Group_User_Group_Groups_Id", - column: x => x.Group_Groups_Id, - principalSchema: "jellyfin", - principalTable: "User", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "Permission", - schema: "jellyfin", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Kind = table.Column(nullable: false), - Value = table.Column(nullable: false), - RowVersion = table.Column(nullable: false), - Permission_GroupPermissions_Id = table.Column(nullable: true), - Permission_Permissions_Id = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Permission", x => x.Id); - table.ForeignKey( - name: "FK_Permission_Group_Permission_GroupPermissions_Id", - column: x => x.Permission_GroupPermissions_Id, - principalSchema: "jellyfin", - principalTable: "Group", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "FK_Permission_User_Permission_Permissions_Id", - column: x => x.Permission_Permissions_Id, - principalSchema: "jellyfin", - principalTable: "User", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "Preference", - schema: "jellyfin", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Kind = table.Column(nullable: false), - Value = table.Column(maxLength: 65535, nullable: false), - RowVersion = table.Column(nullable: false), - Preference_Preferences_Id = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Preference", x => x.Id); - table.ForeignKey( - name: "FK_Preference_Group_Preference_Preferences_Id", - column: x => x.Preference_Preferences_Id, - principalSchema: "jellyfin", - principalTable: "Group", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "FK_Preference_User_Preference_Preferences_Id", - column: x => x.Preference_Preferences_Id, - principalSchema: "jellyfin", - principalTable: "User", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "ProviderMapping", - schema: "jellyfin", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - ProviderName = table.Column(maxLength: 255, nullable: false), - ProviderSecrets = table.Column(maxLength: 65535, nullable: false), - ProviderData = table.Column(maxLength: 65535, nullable: false), - RowVersion = table.Column(nullable: false), - ProviderMapping_ProviderMappings_Id = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_ProviderMapping", x => x.Id); - table.ForeignKey( - name: "FK_ProviderMapping_Group_ProviderMapping_ProviderMappings_Id", - column: x => x.ProviderMapping_ProviderMappings_Id, - principalSchema: "jellyfin", - principalTable: "Group", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "FK_ProviderMapping_User_ProviderMapping_ProviderMappings_Id", - column: x => x.ProviderMapping_ProviderMappings_Id, - principalSchema: "jellyfin", - principalTable: "User", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateIndex( - name: "IX_Group_Group_Groups_Id", - schema: "jellyfin", - table: "Group", - column: "Group_Groups_Id"); - - migrationBuilder.CreateIndex( - name: "IX_Permission_Permission_GroupPermissions_Id", - schema: "jellyfin", - table: "Permission", - column: "Permission_GroupPermissions_Id"); - - migrationBuilder.CreateIndex( - name: "IX_Permission_Permission_Permissions_Id", - schema: "jellyfin", - table: "Permission", - column: "Permission_Permissions_Id"); - - migrationBuilder.CreateIndex( - name: "IX_Preference_Preference_Preferences_Id", - schema: "jellyfin", - table: "Preference", - column: "Preference_Preferences_Id"); - - migrationBuilder.CreateIndex( - name: "IX_ProviderMapping_ProviderMapping_ProviderMappings_Id", - schema: "jellyfin", - table: "ProviderMapping", - column: "ProviderMapping_ProviderMappings_Id"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Permission", - schema: "jellyfin"); - - migrationBuilder.DropTable( - name: "Preference", - schema: "jellyfin"); - - migrationBuilder.DropTable( - name: "ProviderMapping", - schema: "jellyfin"); - - migrationBuilder.DropTable( - name: "Group", - schema: "jellyfin"); - - migrationBuilder.DropTable( - name: "User", - schema: "jellyfin"); - } - } -} diff --git a/Jellyfin.Server.Implementations/Migrations/20200517002411_AddUsers.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200517002411_AddUsers.Designer.cs new file mode 100644 index 000000000..36c58c8ca --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20200517002411_AddUsers.Designer.cs @@ -0,0 +1,404 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1601 + +// +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDb))] + [Migration("20200517002411_AddUsers")] + partial class AddUsers + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("jellyfin") + .HasAnnotation("ProductVersion", "3.1.3"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedule"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("Overview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Group_Groups_Guid") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Group_Groups_Guid"); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ImageInfo"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_GroupPermissions_Id") + .HasColumnType("TEXT"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Permission_GroupPermissions_Id"); + + b.HasIndex("Permission_Permissions_Guid"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("Preference_Preferences_Id") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.HasKey("Id"); + + b.HasIndex("Preference_Preferences_Guid"); + + b.HasIndex("Preference_Preferences_Id"); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ProviderMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ProviderData") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("ProviderMapping_ProviderMappings_Id") + .HasColumnType("TEXT"); + + b.Property("ProviderName") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("ProviderSecrets") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderMapping_ProviderMappings_Id"); + + b.ToTable("ProviderMapping"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EasyPassword") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("ProfileImageId") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.HasKey("Id"); + + b.HasIndex("ProfileImageId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Group", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Groups") + .HasForeignKey("Group_Groups_Guid"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.Group", null) + .WithMany("Permissions") + .HasForeignKey("Permission_GroupPermissions_Id"); + + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("Permission_Permissions_Guid"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("Preference_Preferences_Guid"); + + b.HasOne("Jellyfin.Data.Entities.Group", null) + .WithMany("Preferences") + .HasForeignKey("Preference_Preferences_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ProviderMapping", b => + { + b.HasOne("Jellyfin.Data.Entities.Group", null) + .WithMany("ProviderMappings") + .HasForeignKey("ProviderMapping_ProviderMappings_Id"); + + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ProviderMappings") + .HasForeignKey("ProviderMapping_ProviderMappings_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.HasOne("Jellyfin.Data.Entities.ImageInfo", "ProfileImage") + .WithMany() + .HasForeignKey("ProfileImageId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20200517002411_AddUsers.cs b/Jellyfin.Server.Implementations/Migrations/20200517002411_AddUsers.cs new file mode 100644 index 000000000..55c6f371c --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20200517002411_AddUsers.cs @@ -0,0 +1,297 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1601 + +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Jellyfin.Server.Implementations.Migrations +{ + public partial class AddUsers : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ImageInfo", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Path = table.Column(nullable: false), + LastModified = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ImageInfo", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false), + Username = table.Column(maxLength: 255, nullable: false), + Password = table.Column(maxLength: 65535, nullable: true), + EasyPassword = table.Column(maxLength: 65535, nullable: true), + MustUpdatePassword = table.Column(nullable: false), + AudioLanguagePreference = table.Column(maxLength: 255, nullable: true), + AuthenticationProviderId = table.Column(maxLength: 255, nullable: false), + PasswordResetProviderId = table.Column(maxLength: 255, nullable: false), + InvalidLoginAttemptCount = table.Column(nullable: false), + LastActivityDate = table.Column(nullable: false), + LastLoginDate = table.Column(nullable: false), + LoginAttemptsBeforeLockout = table.Column(nullable: true), + SubtitleMode = table.Column(nullable: false), + PlayDefaultAudioTrack = table.Column(nullable: false), + SubtitleLanguagePreference = table.Column(maxLength: 255, nullable: true), + DisplayMissingEpisodes = table.Column(nullable: false), + DisplayCollectionsView = table.Column(nullable: false), + EnableLocalPassword = table.Column(nullable: false), + HidePlayedInLatest = table.Column(nullable: false), + RememberAudioSelections = table.Column(nullable: false), + RememberSubtitleSelections = table.Column(nullable: false), + EnableNextEpisodeAutoPlay = table.Column(nullable: false), + EnableAutoLogin = table.Column(nullable: false), + EnableUserPreferenceAccess = table.Column(nullable: false), + MaxParentalAgeRating = table.Column(nullable: true), + RemoteClientBitrateLimit = table.Column(nullable: true), + InternalId = table.Column(nullable: false), + ProfileImageId = table.Column(nullable: true), + RowVersion = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + table.ForeignKey( + name: "FK_Users_ImageInfo_ProfileImageId", + column: x => x.ProfileImageId, + principalSchema: "jellyfin", + principalTable: "ImageInfo", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "AccessSchedule", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(nullable: false), + DayOfWeek = table.Column(nullable: false), + StartHour = table.Column(nullable: false), + EndHour = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AccessSchedule", x => x.Id); + table.ForeignKey( + name: "FK_AccessSchedule_Users_UserId", + column: x => x.UserId, + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Groups", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false), + Name = table.Column(maxLength: 255, nullable: false), + RowVersion = table.Column(nullable: false), + Group_Groups_Guid = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Groups", x => x.Id); + table.ForeignKey( + name: "FK_Groups_Users_Group_Groups_Guid", + column: x => x.Group_Groups_Guid, + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Permissions", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Kind = table.Column(nullable: false), + Value = table.Column(nullable: false), + RowVersion = table.Column(nullable: false), + Permission_GroupPermissions_Id = table.Column(nullable: true), + Permission_Permissions_Guid = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Permissions", x => x.Id); + table.ForeignKey( + name: "FK_Permissions_Groups_Permission_GroupPermissions_Id", + column: x => x.Permission_GroupPermissions_Id, + principalSchema: "jellyfin", + principalTable: "Groups", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Permissions_Users_Permission_Permissions_Guid", + column: x => x.Permission_Permissions_Guid, + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Preferences", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Kind = table.Column(nullable: false), + Value = table.Column(maxLength: 65535, nullable: false), + RowVersion = table.Column(nullable: false), + Preference_Preferences_Guid = table.Column(nullable: true), + Preference_Preferences_Id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Preferences", x => x.Id); + table.ForeignKey( + name: "FK_Preferences_Users_Preference_Preferences_Guid", + column: x => x.Preference_Preferences_Guid, + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Preferences_Groups_Preference_Preferences_Id", + column: x => x.Preference_Preferences_Id, + principalSchema: "jellyfin", + principalTable: "Groups", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "ProviderMapping", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ProviderName = table.Column(maxLength: 255, nullable: false), + ProviderSecrets = table.Column(maxLength: 65535, nullable: false), + ProviderData = table.Column(maxLength: 65535, nullable: false), + RowVersion = table.Column(nullable: false), + ProviderMapping_ProviderMappings_Id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ProviderMapping", x => x.Id); + table.ForeignKey( + name: "FK_ProviderMapping_Groups_ProviderMapping_ProviderMappings_Id", + column: x => x.ProviderMapping_ProviderMappings_Id, + principalSchema: "jellyfin", + principalTable: "Groups", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_ProviderMapping_Users_ProviderMapping_ProviderMappings_Id", + column: x => x.ProviderMapping_ProviderMappings_Id, + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_AccessSchedule_UserId", + schema: "jellyfin", + table: "AccessSchedule", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_Groups_Group_Groups_Guid", + schema: "jellyfin", + table: "Groups", + column: "Group_Groups_Guid"); + + migrationBuilder.CreateIndex( + name: "IX_Permissions_Permission_GroupPermissions_Id", + schema: "jellyfin", + table: "Permissions", + column: "Permission_GroupPermissions_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Permissions_Permission_Permissions_Guid", + schema: "jellyfin", + table: "Permissions", + column: "Permission_Permissions_Guid"); + + migrationBuilder.CreateIndex( + name: "IX_Preferences_Preference_Preferences_Guid", + schema: "jellyfin", + table: "Preferences", + column: "Preference_Preferences_Guid"); + + migrationBuilder.CreateIndex( + name: "IX_Preferences_Preference_Preferences_Id", + schema: "jellyfin", + table: "Preferences", + column: "Preference_Preferences_Id"); + + migrationBuilder.CreateIndex( + name: "IX_ProviderMapping_ProviderMapping_ProviderMappings_Id", + schema: "jellyfin", + table: "ProviderMapping", + column: "ProviderMapping_ProviderMappings_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Users_ProfileImageId", + schema: "jellyfin", + table: "Users", + column: "ProfileImageId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AccessSchedule", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "Permissions", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "Preferences", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "ProviderMapping", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "Groups", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "Users", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "ImageInfo", + schema: "jellyfin"); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index 0fb0ba803..46714e865 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -1,7 +1,9 @@ // using System; +using Jellyfin.Server.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Jellyfin.Server.Implementations.Migrations { @@ -15,6 +17,31 @@ namespace Jellyfin.Server.Implementations.Migrations .HasDefaultSchema("jellyfin") .HasAnnotation("ProductVersion", "3.1.3"); + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedule"); + }); + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => { b.Property("Id") @@ -63,12 +90,12 @@ namespace Jellyfin.Server.Implementations.Migrations modelBuilder.Entity("Jellyfin.Data.Entities.Group", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("TEXT"); - b.Property("Group_Groups_Id") - .HasColumnType("INTEGER"); + b.Property("Group_Groups_Guid") + .HasColumnType("TEXT"); b.Property("Name") .IsRequired() @@ -81,9 +108,27 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasKey("Id"); - b.HasIndex("Group_Groups_Id"); + b.HasIndex("Group_Groups_Guid"); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.ToTable("Group"); + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ImageInfo"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => @@ -95,11 +140,11 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property("Kind") .HasColumnType("INTEGER"); - b.Property("Permission_GroupPermissions_Id") - .HasColumnType("INTEGER"); + b.Property("Permission_GroupPermissions_Id") + .HasColumnType("TEXT"); - b.Property("Permission_Permissions_Id") - .HasColumnType("INTEGER"); + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); b.Property("RowVersion") .IsConcurrencyToken() @@ -112,9 +157,9 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("Permission_GroupPermissions_Id"); - b.HasIndex("Permission_Permissions_Id"); + b.HasIndex("Permission_Permissions_Guid"); - b.ToTable("Permission"); + b.ToTable("Permissions"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => @@ -126,8 +171,11 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property("Kind") .HasColumnType("INTEGER"); - b.Property("Preference_Preferences_Id") - .HasColumnType("INTEGER"); + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("Preference_Preferences_Id") + .HasColumnType("TEXT"); b.Property("RowVersion") .IsConcurrencyToken() @@ -140,9 +188,11 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasKey("Id"); + b.HasIndex("Preference_Preferences_Guid"); + b.HasIndex("Preference_Preferences_Id"); - b.ToTable("Preference"); + b.ToTable("Preferences"); }); modelBuilder.Entity("Jellyfin.Data.Entities.ProviderMapping", b => @@ -156,8 +206,8 @@ namespace Jellyfin.Server.Implementations.Migrations .HasColumnType("TEXT") .HasMaxLength(65535); - b.Property("ProviderMapping_ProviderMappings_Id") - .HasColumnType("INTEGER"); + b.Property("ProviderMapping_ProviderMappings_Id") + .HasColumnType("TEXT"); b.Property("ProviderName") .IsRequired() @@ -182,12 +232,11 @@ namespace Jellyfin.Server.Implementations.Migrations modelBuilder.Entity("Jellyfin.Data.Entities.User", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("TEXT"); b.Property("AudioLanguagePreference") - .IsRequired() .HasColumnType("TEXT") .HasMaxLength(255); @@ -196,71 +245,86 @@ namespace Jellyfin.Server.Implementations.Migrations .HasColumnType("TEXT") .HasMaxLength(255); - b.Property("DisplayCollectionsView") + b.Property("DisplayCollectionsView") .HasColumnType("INTEGER"); - b.Property("DisplayMissingEpisodes") + b.Property("DisplayMissingEpisodes") .HasColumnType("INTEGER"); - b.Property("EnableNextEpisodeAutoPlay") + b.Property("EasyPassword") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("EnableAutoLogin") .HasColumnType("INTEGER"); - b.Property("EnableUserPreferenceAccess") + b.Property("EnableLocalPassword") .HasColumnType("INTEGER"); - b.Property("GroupedFolders") - .HasColumnType("TEXT") - .HasMaxLength(65535); + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); - b.Property("HidePlayedInLatest") + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") .HasColumnType("INTEGER"); b.Property("InvalidLoginAttemptCount") .HasColumnType("INTEGER"); - b.Property("LatestItemExcludes") - .HasColumnType("TEXT") - .HasMaxLength(65535); + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); b.Property("LoginAttemptsBeforeLockout") .HasColumnType("INTEGER"); - b.Property("MustUpdatePassword") + b.Property("MaxParentalAgeRating") .HasColumnType("INTEGER"); - b.Property("MyMediaExcludes") - .HasColumnType("TEXT") - .HasMaxLength(65535); + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); - b.Property("OrderedViews") + b.Property("Password") .HasColumnType("TEXT") .HasMaxLength(65535); - b.Property("Password") + b.Property("PasswordResetProviderId") + .IsRequired() .HasColumnType("TEXT") - .HasMaxLength(65535); + .HasMaxLength(255); b.Property("PlayDefaultAudioTrack") .HasColumnType("INTEGER"); - b.Property("RememberAudioSelections") + b.Property("ProfileImageId") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") .HasColumnType("INTEGER"); - b.Property("RememberSubtitleSelections") + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") .HasColumnType("INTEGER"); b.Property("RowVersion") .IsConcurrencyToken() .HasColumnType("INTEGER"); - b.Property("SubtitleLanguagePrefernce") + b.Property("SubtitleLanguagePreference") .HasColumnType("TEXT") .HasMaxLength(255); - b.Property("SubtitleMode") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(255); + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); b.Property("Username") .IsRequired() @@ -269,34 +333,45 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasKey("Id"); - b.ToTable("User"); + b.HasIndex("ProfileImageId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); modelBuilder.Entity("Jellyfin.Data.Entities.Group", b => { b.HasOne("Jellyfin.Data.Entities.User", null) .WithMany("Groups") - .HasForeignKey("Group_Groups_Id"); + .HasForeignKey("Group_Groups_Guid"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => { b.HasOne("Jellyfin.Data.Entities.Group", null) - .WithMany("GroupPermissions") + .WithMany("Permissions") .HasForeignKey("Permission_GroupPermissions_Id"); b.HasOne("Jellyfin.Data.Entities.User", null) .WithMany("Permissions") - .HasForeignKey("Permission_Permissions_Id"); + .HasForeignKey("Permission_Permissions_Guid"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => { - b.HasOne("Jellyfin.Data.Entities.Group", null) + b.HasOne("Jellyfin.Data.Entities.User", null) .WithMany("Preferences") - .HasForeignKey("Preference_Preferences_Id"); + .HasForeignKey("Preference_Preferences_Guid"); - b.HasOne("Jellyfin.Data.Entities.User", null) + b.HasOne("Jellyfin.Data.Entities.Group", null) .WithMany("Preferences") .HasForeignKey("Preference_Preferences_Id"); }); @@ -311,6 +386,13 @@ namespace Jellyfin.Server.Implementations.Migrations .WithMany("ProviderMappings") .HasForeignKey("ProviderMapping_ProviderMappings_Id"); }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.HasOne("Jellyfin.Data.Entities.ImageInfo", "ProfileImage") + .WithMany() + .HasForeignKey("ProfileImageId"); + }); #pragma warning restore 612, 618 } } diff --git a/Jellyfin.Server.Implementations/User/DefaultAuthenticationProvider.cs b/Jellyfin.Server.Implementations/User/DefaultAuthenticationProvider.cs deleted file mode 100644 index 024500bf8..000000000 --- a/Jellyfin.Server.Implementations/User/DefaultAuthenticationProvider.cs +++ /dev/null @@ -1,185 +0,0 @@ -using System; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using MediaBrowser.Common; -using MediaBrowser.Common.Cryptography; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Model.Cryptography; - -namespace Jellyfin.Server.Implementations.User -{ - /// - /// The default authentication provider. - /// - public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser - { - private readonly ICryptoProvider _cryptographyProvider; - - /// - /// Initializes a new instance of the class. - /// - /// The cryptography provider. - public DefaultAuthenticationProvider(ICryptoProvider cryptographyProvider) - { - _cryptographyProvider = cryptographyProvider; - } - - /// - public string Name => "Default"; - - /// - public bool IsEnabled => true; - - /// - // This is dumb and an artifact of the backwards way auth providers were designed. - // This version of authenticate was never meant to be called, but needs to be here for interface compat - // Only the providers that don't provide local user support use this - public Task Authenticate(string username, string password) - { - throw new NotImplementedException(); - } - - /// - // This is the version that we need to use for local users. Because reasons. - public Task Authenticate(string username, string password, Data.Entities.User resolvedUser) - { - if (resolvedUser == null) - { - throw new AuthenticationException("Specified user does not exist."); - } - - bool success = false; - - // As long as jellyfin supports passwordless users, we need this little block here to accommodate - if (!HasPassword(resolvedUser)) - { - return Task.FromResult(new ProviderAuthenticationResult - { - Username = username - }); - } - - byte[] passwordBytes = Encoding.UTF8.GetBytes(password); - - PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password); - if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id) - || _cryptographyProvider.DefaultHashMethod == readyHash.Id) - { - byte[] calculatedHash = _cryptographyProvider.ComputeHash( - readyHash.Id, - passwordBytes, - readyHash.Salt.ToArray()); - - if (readyHash.Hash.SequenceEqual(calculatedHash)) - { - success = true; - } - } - else - { - throw new AuthenticationException($"Requested crypto method not available in provider: {readyHash.Id}"); - } - - if (!success) - { - throw new AuthenticationException("Invalid username or password"); - } - - return Task.FromResult(new ProviderAuthenticationResult - { - Username = username - }); - } - - /// - public bool HasPassword(Data.Entities.User user) - => !string.IsNullOrEmpty(user.Password); - - /// - public Task ChangePassword(Data.Entities.User user, string newPassword) - { - if (string.IsNullOrEmpty(newPassword)) - { - user.Password = null; - return Task.CompletedTask; - } - - PasswordHash newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword); - user.Password = newPasswordHash.ToString(); - - return Task.CompletedTask; - } - - /// - public void ChangeEasyPassword(Data.Entities.User user, string newPassword, string newPasswordHash) - { - if (newPassword != null) - { - newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword).ToString(); - } - - if (string.IsNullOrWhiteSpace(newPasswordHash)) - { - throw new ArgumentNullException(nameof(newPasswordHash)); - } - - user.EasyPassword = newPasswordHash; - } - - /// - public string GetEasyPasswordHash(Data.Entities.User user) - { - return string.IsNullOrEmpty(user.EasyPassword) - ? null - : Hex.Encode(PasswordHash.Parse(user.EasyPassword).Hash); - } - - /// - /// Hashes the provided string. - /// - /// The user. - /// The string to hash. - /// The hashed string. - public string GetHashedString(Data.Entities.User user, string str) - { - if (string.IsNullOrEmpty(user.Password)) - { - return _cryptographyProvider.CreatePasswordHash(str).ToString(); - } - - // TODO: make use of iterations parameter? - PasswordHash passwordHash = PasswordHash.Parse(user.Password); - var salt = passwordHash.Salt.ToArray(); - return new PasswordHash( - passwordHash.Id, - _cryptographyProvider.ComputeHash( - passwordHash.Id, - Encoding.UTF8.GetBytes(str), - salt), - salt, - passwordHash.Parameters.ToDictionary(x => x.Key, y => y.Value)).ToString(); - } - - /// - /// Hashes the provided string. - /// - /// The user. - /// The string to hash. - /// The hashed string. - public ReadOnlySpan GetHashed(Data.Entities.User user, string str) - { - if (string.IsNullOrEmpty(user.Password)) - { - return _cryptographyProvider.CreatePasswordHash(str).Hash; - } - - // TODO: make use of iterations parameter? - PasswordHash passwordHash = PasswordHash.Parse(user.Password); - return _cryptographyProvider.ComputeHash( - passwordHash.Id, - Encoding.UTF8.GetBytes(str), - passwordHash.Salt.ToArray()); - } - } -} diff --git a/Jellyfin.Server.Implementations/User/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/User/DefaultPasswordResetProvider.cs deleted file mode 100644 index 80ab3ce00..000000000 --- a/Jellyfin.Server.Implementations/User/DefaultPasswordResetProvider.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Security.Cryptography; -using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; - -namespace Jellyfin.Server.Implementations.User -{ - /// - /// The default password reset provider. - /// - public class DefaultPasswordResetProvider : IPasswordResetProvider - { - private const string BaseResetFileName = "passwordreset"; - - private readonly IJsonSerializer _jsonSerializer; - private readonly IUserManager _userManager; - - private readonly string _passwordResetFileBase; - private readonly string _passwordResetFileBaseDir; - - /// - /// Initializes a new instance of the class. - /// - /// The configuration manager. - /// The JSON serializer. - /// The user manager. - public DefaultPasswordResetProvider( - IServerConfigurationManager configurationManager, - IJsonSerializer jsonSerializer, - IUserManager userManager) - { - _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath; - _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName); - _jsonSerializer = jsonSerializer; - _userManager = userManager; - } - - /// - public string Name => "Default Password Reset Provider"; - - /// - public bool IsEnabled => true; - - /// - public async Task RedeemPasswordResetPin(string pin) - { - SerializablePasswordReset spr; - List usersReset = new List(); - foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*")) - { - await using (var str = File.OpenRead(resetFile)) - { - spr = await _jsonSerializer.DeserializeFromStreamAsync(str).ConfigureAwait(false); - } - - if (spr.ExpirationDate < DateTime.Now) - { - File.Delete(resetFile); - } - else if (string.Equals( - spr.Pin.Replace("-", string.Empty, StringComparison.Ordinal), - pin.Replace("-", string.Empty, StringComparison.Ordinal), - StringComparison.InvariantCultureIgnoreCase)) - { - var resetUser = _userManager.GetUserByName(spr.UserName); - if (resetUser == null) - { - throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found"); - } - - await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); - usersReset.Add(resetUser.Username); - File.Delete(resetFile); - } - } - - if (usersReset.Count < 1) - { - throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}"); - } - - return new PinRedeemResult - { - Success = true, - UsersReset = usersReset.ToArray() - }; - } - - /// - public async Task StartForgotPasswordProcess(Jellyfin.Data.Entities.User user, bool isInNetwork) - { - string pin; - using (var cryptoRandom = RandomNumberGenerator.Create()) - { - byte[] bytes = new byte[4]; - cryptoRandom.GetBytes(bytes); - pin = BitConverter.ToString(bytes); - } - - DateTime expireTime = DateTime.Now.AddMinutes(30); - - user.EasyPassword = pin; - await _userManager.UpdateUserAsync(user).ConfigureAwait(false); - - return new ForgotPasswordResult - { - Action = ForgotPasswordAction.PinCode, - PinExpirationDate = expireTime, - }; - } - - private class SerializablePasswordReset : PasswordPinCreationResult - { - public string Pin { get; set; } - - public string UserName { get; set; } - } - } -} diff --git a/Jellyfin.Server.Implementations/User/DeviceAccessEntryPoint.cs b/Jellyfin.Server.Implementations/User/DeviceAccessEntryPoint.cs deleted file mode 100644 index d33034ab2..000000000 --- a/Jellyfin.Server.Implementations/User/DeviceAccessEntryPoint.cs +++ /dev/null @@ -1,65 +0,0 @@ -#pragma warning disable CS1591 - -using System.Threading.Tasks; -using Jellyfin.Data.Enums; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Controller.Security; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Events; - -namespace Jellyfin.Server.Implementations.User -{ - public sealed class DeviceAccessEntryPoint : IServerEntryPoint - { - private readonly IUserManager _userManager; - private readonly IAuthenticationRepository _authRepo; - private readonly IDeviceManager _deviceManager; - private readonly ISessionManager _sessionManager; - - public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager) - { - _userManager = userManager; - _authRepo = authRepo; - _deviceManager = deviceManager; - _sessionManager = sessionManager; - } - - public Task RunAsync() - { - _userManager.OnUserUpdated += OnUserUpdated; - - return Task.CompletedTask; - } - - private void OnUserUpdated(object sender, GenericEventArgs e) - { - var user = e.Argument; - if (!user.HasPermission(PermissionKind.EnableAllDevices)) - { - UpdateDeviceAccess(user); - } - } - - public void Dispose() - { - } - - private void UpdateDeviceAccess(Data.Entities.User user) - { - var existing = _authRepo.Get(new AuthenticationInfoQuery - { - UserId = user.Id - }).Items; - - foreach (var authInfo in existing) - { - if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId)) - { - _sessionManager.Logout(authInfo); - } - } - } - } -} diff --git a/Jellyfin.Server.Implementations/User/InvalidAuthProvider.cs b/Jellyfin.Server.Implementations/User/InvalidAuthProvider.cs deleted file mode 100644 index a11ca128a..000000000 --- a/Jellyfin.Server.Implementations/User/InvalidAuthProvider.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Controller.Authentication; - -namespace Jellyfin.Server.Implementations.User -{ - /// - /// An invalid authentication provider. - /// - public class InvalidAuthProvider : IAuthenticationProvider - { - /// - public string Name => "InvalidOrMissingAuthenticationProvider"; - - /// - public bool IsEnabled => true; - - /// - public Task Authenticate(string username, string password) - { - throw new AuthenticationException("User Account cannot login with this provider. The Normal provider for this user cannot be found"); - } - - /// - public bool HasPassword(Data.Entities.User user) - { - return true; - } - - /// - public Task ChangePassword(Data.Entities.User user, string newPassword) - { - return Task.CompletedTask; - } - - /// - public void ChangeEasyPassword(Data.Entities.User user, string newPassword, string newPasswordHash) - { - // Nothing here - } - - /// - public string GetEasyPasswordHash(Data.Entities.User user) - { - return string.Empty; - } - } -} diff --git a/Jellyfin.Server.Implementations/User/UserManager.cs b/Jellyfin.Server.Implementations/User/UserManager.cs deleted file mode 100644 index 73905ff70..000000000 --- a/Jellyfin.Server.Implementations/User/UserManager.cs +++ /dev/null @@ -1,771 +0,0 @@ -#pragma warning disable CS0067 -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Jellyfin.Data.Enums; -using MediaBrowser.Common.Cryptography; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Cryptography; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Users; -using Microsoft.Extensions.Logging; - -namespace Jellyfin.Server.Implementations.User -{ - public class UserManager : IUserManager - { - private readonly JellyfinDbProvider _dbProvider; - private readonly ICryptoProvider _cryptoProvider; - private readonly INetworkManager _networkManager; - private readonly ILogger _logger; - - private IAuthenticationProvider[] _authenticationProviders; - private DefaultAuthenticationProvider _defaultAuthenticationProvider; - private InvalidAuthProvider _invalidAuthProvider; - private IPasswordResetProvider[] _passwordResetProviders; - private DefaultPasswordResetProvider _defaultPasswordResetProvider; - - public UserManager( - JellyfinDbProvider dbProvider, - ICryptoProvider cryptoProvider, - INetworkManager networkManager, - ILogger logger) - { - _dbProvider = dbProvider; - _cryptoProvider = cryptoProvider; - _networkManager = networkManager; - _logger = logger; - } - - public event EventHandler> OnUserPasswordChanged; - - /// - public event EventHandler> OnUserUpdated; - - /// - public event EventHandler> OnUserCreated; - - /// - public event EventHandler> OnUserDeleted; - - public event EventHandler> OnUserLockedOut; - - public IEnumerable Users - { - get - { - using var dbContext = _dbProvider.CreateContext(); - return dbContext.Users; - } - } - - public IEnumerable UsersIds - { - get - { - using var dbContext = _dbProvider.CreateContext(); - return dbContext.Users.Select(u => u.Id); - } - } - - public Data.Entities.User GetUserById(Guid id) - { - if (id == Guid.Empty) - { - throw new ArgumentException("Guid can't be empty", nameof(id)); - } - - using var dbContext = _dbProvider.CreateContext(); - - return dbContext.Users.Find(id); - } - - public Data.Entities.User GetUserByName(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentException("Invalid username", nameof(name)); - } - - using var dbContext = _dbProvider.CreateContext(); - - return dbContext.Users.FirstOrDefault(u => - string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase)); - } - - public async Task RenameUser(Data.Entities.User user, string newName) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (string.IsNullOrWhiteSpace(newName)) - { - throw new ArgumentException("Invalid username", nameof(newName)); - } - - if (user.Username.Equals(newName, StringComparison.Ordinal)) - { - throw new ArgumentException("The new and old names must be different."); - } - - if (Users.Any( - u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.OrdinalIgnoreCase))) - { - throw new ArgumentException(string.Format( - CultureInfo.InvariantCulture, - "A user with the name '{0}' already exists.", - newName)); - } - - user.Username = newName; - await UpdateUserAsync(user).ConfigureAwait(false); - - OnUserUpdated?.Invoke(this, new GenericEventArgs(user)); - } - - public void UpdateUser(Data.Entities.User user) - { - using var dbContext = _dbProvider.CreateContext(); - dbContext.Users.Update(user); - dbContext.SaveChanges(); - } - - public async Task UpdateUserAsync(Data.Entities.User user) - { - await using var dbContext = _dbProvider.CreateContext(); - dbContext.Users.Update(user); - - await dbContext.SaveChangesAsync().ConfigureAwait(false); - } - - public Data.Entities.User CreateUser(string name) - { - using var dbContext = _dbProvider.CreateContext(); - - var newUser = CreateUserObject(name); - dbContext.Users.Add(newUser); - dbContext.SaveChanges(); - - return newUser; - } - - public void DeleteUser(Data.Entities.User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - using var dbContext = _dbProvider.CreateContext(); - - if (!dbContext.Users.Contains(user)) - { - throw new ArgumentException(string.Format( - CultureInfo.InvariantCulture, - "The user cannot be deleted because there is no user with the Name {0} and Id {1}.", - user.Username, - user.Id)); - } - - if (dbContext.Users.Count() == 1) - { - throw new InvalidOperationException(string.Format( - CultureInfo.InvariantCulture, - "The user '{0}' cannot be deleted because there must be at least one user in the system.", - user.Username)); - } - - if (user.HasPermission(PermissionKind.IsAdministrator) - && Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1) - { - throw new ArgumentException( - string.Format( - CultureInfo.InvariantCulture, - "The user '{0}' cannot be deleted because there must be at least one admin user in the system.", - user.Username), - nameof(user)); - } - - dbContext.Users.Remove(user); - dbContext.SaveChanges(); - } - - public Task ResetPassword(Data.Entities.User user) - { - return ChangePassword(user, string.Empty); - } - - public void ResetEasyPassword(Data.Entities.User user) - { - ChangeEasyPassword(user, string.Empty, null); - } - - public async Task ChangePassword(Data.Entities.User user, string newPassword) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false); - await UpdateUserAsync(user).ConfigureAwait(false); - - OnUserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); - } - - public void ChangeEasyPassword(Data.Entities.User user, string newPassword, string newPasswordSha1) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - GetAuthenticationProvider(user).ChangeEasyPassword(user, newPassword, newPasswordSha1); - - UpdateUser(user); - - OnUserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); - } - - public UserDto GetUserDto(Data.Entities.User user, string remoteEndPoint = null) - { - return new UserDto - { - Id = user.Id, - HasPassword = user.Password == null, - EnableAutoLogin = user.EnableAutoLogin, - LastLoginDate = user.LastLoginDate, - LastActivityDate = user.LastActivityDate, - Configuration = new UserConfiguration - { - SubtitleMode = user.SubtitleMode, - HidePlayedInLatest = user.HidePlayedInLatest, - EnableLocalPassword = user.EnableLocalPassword, - PlayDefaultAudioTrack = user.PlayDefaultAudioTrack, - DisplayCollectionsView = user.DisplayCollectionsView, - DisplayMissingEpisodes = user.DisplayMissingEpisodes, - AudioLanguagePreference = user.AudioLanguagePreference, - RememberAudioSelections = user.RememberAudioSelections, - EnableNextEpisodeAutoPlay = user.EnableNextEpisodeAutoPlay, - RememberSubtitleSelections = user.RememberSubtitleSelections, - SubtitleLanguagePreference = user.SubtitleLanguagePreference, - OrderedViews = user.GetPreference(PreferenceKind.OrderedViews), - GroupedFolders = user.GetPreference(PreferenceKind.GroupedFolders), - MyMediaExcludes = user.GetPreference(PreferenceKind.MyMediaExcludes), - LatestItemsExcludes = user.GetPreference(PreferenceKind.LatestItemExcludes) - }, - Policy = new UserPolicy - { - MaxParentalRating = user.MaxParentalAgeRating, - EnableUserPreferenceAccess = user.EnableUserPreferenceAccess, - RemoteClientBitrateLimit = user.RemoteClientBitrateLimit.GetValueOrDefault(), - AuthenticatioIsnProviderId = user.AuthenticationProviderId, - PasswordResetProviderId = user.PasswordResetProviderId, - InvalidLoginAttemptCount = user.InvalidLoginAttemptCount, - LoginAttemptsBeforeLockout = user.LoginAttemptsBeforeLockout.GetValueOrDefault(), - IsAdministrator = user.HasPermission(PermissionKind.IsAdministrator), - IsHidden = user.HasPermission(PermissionKind.IsHidden), - IsDisabled = user.HasPermission(PermissionKind.IsDisabled), - EnableSharedDeviceControl = user.HasPermission(PermissionKind.EnableSharedDeviceControl), - EnableRemoteAccess = user.HasPermission(PermissionKind.EnableRemoteAccess), - EnableLiveTvManagement = user.HasPermission(PermissionKind.EnableLiveTvManagement), - EnableLiveTvAccess = user.HasPermission(PermissionKind.EnableLiveTvAccess), - EnableMediaPlayback = user.HasPermission(PermissionKind.EnableMediaPlayback), - EnableAudioPlaybackTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding), - EnableVideoPlaybackTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding), - EnableContentDeletion = user.HasPermission(PermissionKind.EnableContentDeletion), - EnableContentDownloading = user.HasPermission(PermissionKind.EnableContentDownloading), - EnableSyncTranscoding = user.HasPermission(PermissionKind.EnableSyncTranscoding), - EnableMediaConversion = user.HasPermission(PermissionKind.EnableMediaConversion), - EnableAllChannels = user.HasPermission(PermissionKind.EnableAllChannels), - EnableAllDevices = user.HasPermission(PermissionKind.EnableAllDevices), - EnableAllFolders = user.HasPermission(PermissionKind.EnableAllFolders), - EnableRemoteControlOfOtherUsers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers), - EnablePlaybackRemuxing = user.HasPermission(PermissionKind.EnablePlaybackRemuxing), - ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding), - EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing), - AccessSchedules = user.AccessSchedules.ToArray(), - BlockedTags = user.GetPreference(PreferenceKind.BlockedTags), - EnabledChannels = user.GetPreference(PreferenceKind.EnabledChannels), - EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices), - EnabledFolders = user.GetPreference(PreferenceKind.EnabledFolders), - EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders) - } - }; - } - - public PublicUserDto GetPublicUserDto(Data.Entities.User user, string remoteEndPoint = null) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user); - bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(GetAuthenticationProvider(user).GetEasyPasswordHash(user)); - - bool hasPassword = user.EnableLocalPassword && - !string.IsNullOrEmpty(remoteEndPoint) && - _networkManager.IsInLocalNetwork(remoteEndPoint) ? hasConfiguredEasyPassword : hasConfiguredPassword; - - return new PublicUserDto - { - Name = user.Username, - HasPassword = hasPassword, - HasConfiguredPassword = hasConfiguredPassword - }; - } - - public async Task AuthenticateUser( - string username, - string password, - string passwordSha1, - string remoteEndPoint, - bool isUserSession) - { - if (string.IsNullOrWhiteSpace(username)) - { - _logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndPoint); - throw new ArgumentNullException(nameof(username)); - } - - var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); - bool success; - IAuthenticationProvider authenticationProvider; - - if (user != null) - { - var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint) - .ConfigureAwait(false); - authenticationProvider = authResult.authenticationProvider; - success = authResult.success; - } - else - { - var authResult = await AuthenticateLocalUser(username, password, null, remoteEndPoint) - .ConfigureAwait(false); - authenticationProvider = authResult.authenticationProvider; - string updatedUsername = authResult.username; - success = authResult.success; - - if (success - && authenticationProvider != null - && !(authenticationProvider is DefaultAuthenticationProvider)) - { - // Trust the username returned by the authentication provider - username = updatedUsername; - - // Search the database for the user again - // the authentication provider might have created it - user = Users - .FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); - - if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy) - { - UpdatePolicy(user.Id, hasNewUserPolicy.GetNewUserPolicy()); - - await UpdateUserAsync(user).ConfigureAwait(false); - } - } - } - - if (success && user != null && authenticationProvider != null) - { - var providerId = authenticationProvider.GetType().FullName; - - if (!string.Equals(providerId, user.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase)) - { - user.AuthenticationProviderId = providerId; - await UpdateUserAsync(user).ConfigureAwait(false); - } - } - - if (user == null) - { - _logger.LogInformation( - "Authentication request for {UserName} has been denied (IP: {IP}).", - username, - remoteEndPoint); - throw new AuthenticationException("Invalid username or password entered."); - } - - if (user.HasPermission(PermissionKind.IsDisabled)) - { - _logger.LogInformation( - "Authentication request for {UserName} has been denied because this account is currently disabled (IP: {IP}).", - username, - remoteEndPoint); - throw new SecurityException( - $"The {user.Username} account is currently disabled. Please consult with your administrator."); - } - - if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && - !_networkManager.IsInLocalNetwork(remoteEndPoint)) - { - _logger.LogInformation( - "Authentication request for {UserName} forbidden: remote access disabled and user not in local network (IP: {IP}).", - username, - remoteEndPoint); - throw new SecurityException("Forbidden."); - } - - if (!user.IsParentalScheduleAllowed()) - { - _logger.LogInformation( - "Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {IP}).", - username, - remoteEndPoint); - throw new SecurityException("User is not allowed access at this time."); - } - - // Update LastActivityDate and LastLoginDate, then save - if (success) - { - if (isUserSession) - { - user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; - UpdateUser(user); - } - - ResetInvalidLoginAttemptCount(user); - _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Username); - } - else - { - IncrementInvalidLoginAttemptCount(user); - _logger.LogInformation( - "Authentication request for {UserName} has been denied (IP: {IP}).", - user.Username, - remoteEndPoint); - } - - return success ? user : null; - } - - public async Task StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) - { - var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername); - - var action = ForgotPasswordAction.InNetworkRequired; - - if (user != null && isInNetwork) - { - var passwordResetProvider = GetPasswordResetProvider(user); - return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false); - } - - return new ForgotPasswordResult - { - Action = action, - PinFile = string.Empty - }; - } - - public async Task RedeemPasswordResetPin(string pin) - { - foreach (var provider in _passwordResetProviders) - { - var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false); - - if (result.Success) - { - return result; - } - } - - return new PinRedeemResult - { - Success = false, - UsersReset = Array.Empty() - }; - } - - public void AddParts(IEnumerable authenticationProviders, IEnumerable passwordResetProviders) - { - _authenticationProviders = authenticationProviders.ToArray(); - - _defaultAuthenticationProvider = _authenticationProviders.OfType().First(); - - _invalidAuthProvider = _authenticationProviders.OfType().First(); - - _passwordResetProviders = passwordResetProviders.ToArray(); - - _defaultPasswordResetProvider = passwordResetProviders.OfType().First(); - } - - public NameIdPair[] GetAuthenticationProviders() - { - return _authenticationProviders - .Where(provider => provider.IsEnabled) - .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1) - .ThenBy(i => i.Name) - .Select(i => new NameIdPair - { - Name = i.Name, - Id = i.GetType().FullName - }) - .ToArray(); - } - - public NameIdPair[] GetPasswordResetProviders() - { - return _passwordResetProviders - .Where(provider => provider.IsEnabled) - .OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1) - .ThenBy(i => i.Name) - .Select(i => new NameIdPair - { - Name = i.Name, - Id = i.GetType().FullName - }) - .ToArray(); - } - - public void UpdateConfiguration(Guid userId, UserConfiguration config) - { - var user = GetUserById(userId); - user.SubtitleMode = config.SubtitleMode; - user.HidePlayedInLatest = config.HidePlayedInLatest; - user.EnableLocalPassword = config.EnableLocalPassword; - user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack; - user.DisplayCollectionsView = config.DisplayCollectionsView; - user.DisplayMissingEpisodes = config.DisplayMissingEpisodes; - user.AudioLanguagePreference = config.AudioLanguagePreference; - user.RememberAudioSelections = config.RememberAudioSelections; - user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay; - user.RememberSubtitleSelections = config.RememberSubtitleSelections; - user.SubtitleLanguagePreference = config.SubtitleLanguagePreference; - - user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews); - user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders); - user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes); - user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes); - - UpdateUser(user); - } - - public void UpdatePolicy(Guid userId, UserPolicy policy) - { - var user = GetUserById(userId); - - user.MaxParentalAgeRating = policy.MaxParentalRating; - user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess; - user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit; - user.AuthenticationProviderId = policy.AuthenticatioIsnProviderId; - user.PasswordResetProviderId = policy.PasswordResetProviderId; - user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount; - user.LoginAttemptsBeforeLockout = policy.LoginAttemptsBeforeLockout == -1 - ? null - : new int?(policy.LoginAttemptsBeforeLockout); - user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator); - user.SetPermission(PermissionKind.IsHidden, policy.IsHidden); - user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled); - user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl); - user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess); - user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement); - user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess); - user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback); - user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding); - user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding); - user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion); - user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading); - user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding); - user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion); - user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels); - user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices); - user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders); - user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers); - user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing); - user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding); - user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing); - - user.AccessSchedules.Clear(); - foreach (var policyAccessSchedule in policy.AccessSchedules) - { - user.AccessSchedules.Add(policyAccessSchedule); - } - - user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags); - user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels); - user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices); - user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders); - user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders); - } - - private Data.Entities.User CreateUserObject(string name) - { - return new Data.Entities.User( - username: name, - mustUpdatePassword: false, - authenticationProviderId: _defaultAuthenticationProvider.GetType().FullName, - invalidLoginAttemptCount: -1, - subtitleMode: SubtitlePlaybackMode.Default, - playDefaultAudioTrack: true); - } - - private IAuthenticationProvider GetAuthenticationProvider(Data.Entities.User user) - { - return GetAuthenticationProviders(user)[0]; - } - - private IPasswordResetProvider GetPasswordResetProvider(Data.Entities.User user) - { - return GetPasswordResetProviders(user)[0]; - } - - private IList GetAuthenticationProviders(Data.Entities.User user) - { - var authenticationProviderId = user?.AuthenticationProviderId; - - var providers = _authenticationProviders.Where(i => i.IsEnabled).ToList(); - - if (!string.IsNullOrEmpty(authenticationProviderId)) - { - providers = providers.Where(i => string.Equals(authenticationProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase)).ToList(); - } - - if (providers.Count == 0) - { - // Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found - _logger.LogWarning( - "User {UserName} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. Assigning user to InvalidAuthProvider until this is corrected", - user?.Username, - user?.AuthenticationProviderId); - providers = new List - { - _invalidAuthProvider - }; - } - - return providers; - } - - private IList GetPasswordResetProviders(Data.Entities.User user) - { - var passwordResetProviderId = user?.PasswordResetProviderId; - var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray(); - - if (!string.IsNullOrEmpty(passwordResetProviderId)) - { - providers = providers.Where(i => - string.Equals(passwordResetProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase)) - .ToArray(); - } - - if (providers.Length == 0) - { - providers = new IPasswordResetProvider[] - { - _defaultPasswordResetProvider - }; - } - - return providers; - } - - private async Task<(IAuthenticationProvider authenticationProvider, string username, bool success)> - AuthenticateLocalUser( - string username, - string password, - Jellyfin.Data.Entities.User user, - string remoteEndPoint) - { - bool success = false; - IAuthenticationProvider authenticationProvider = null; - - foreach (var provider in GetAuthenticationProviders(user)) - { - var providerAuthResult = - await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); - var updatedUsername = providerAuthResult.username; - success = providerAuthResult.success; - - if (success) - { - authenticationProvider = provider; - username = updatedUsername; - break; - } - } - - if (!success - && _networkManager.IsInLocalNetwork(remoteEndPoint) - && user?.EnableLocalPassword == true - && !string.IsNullOrEmpty(user.EasyPassword)) - { - // Check easy password - var passwordHash = PasswordHash.Parse(user.EasyPassword); - var hash = _cryptoProvider.ComputeHash( - passwordHash.Id, - Encoding.UTF8.GetBytes(password), - passwordHash.Salt.ToArray()); - success = passwordHash.Hash.SequenceEqual(hash); - } - - return (authenticationProvider, username, success); - } - - private async Task<(string username, bool success)> AuthenticateWithProvider( - IAuthenticationProvider provider, - string username, - string password, - Data.Entities.User resolvedUser) - { - try - { - var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser - ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false) - : await provider.Authenticate(username, password).ConfigureAwait(false); - - if (authenticationResult.Username != username) - { - _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username); - username = authenticationResult.Username; - } - - return (username, true); - } - catch (AuthenticationException ex) - { - _logger.LogError(ex, "Error authenticating with provider {Provider}", provider.Name); - - return (username, false); - } - } - - private void IncrementInvalidLoginAttemptCount(Data.Entities.User user) - { - int invalidLogins = user.InvalidLoginAttemptCount; - int? maxInvalidLogins = user.LoginAttemptsBeforeLockout; - if (maxInvalidLogins.HasValue - && invalidLogins >= maxInvalidLogins) - { - user.SetPermission(PermissionKind.IsDisabled, true); - OnUserLockedOut?.Invoke(this, new GenericEventArgs(user)); - _logger.LogWarning( - "Disabling user {UserName} due to {Attempts} unsuccessful login attempts.", - user.Username, - invalidLogins); - } - - UpdateUser(user); - } - - private void ResetInvalidLoginAttemptCount(Data.Entities.User user) - { - user.InvalidLoginAttemptCount = 0; - } - } -} diff --git a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs new file mode 100644 index 000000000..38494727c --- /dev/null +++ b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs @@ -0,0 +1,185 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Common; +using MediaBrowser.Common.Cryptography; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Model.Cryptography; + +namespace Jellyfin.Server.Implementations.Users +{ + /// + /// The default authentication provider. + /// + public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser + { + private readonly ICryptoProvider _cryptographyProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The cryptography provider. + public DefaultAuthenticationProvider(ICryptoProvider cryptographyProvider) + { + _cryptographyProvider = cryptographyProvider; + } + + /// + public string Name => "Default"; + + /// + public bool IsEnabled => true; + + /// + // This is dumb and an artifact of the backwards way auth providers were designed. + // This version of authenticate was never meant to be called, but needs to be here for interface compat + // Only the providers that don't provide local user support use this + public Task Authenticate(string username, string password) + { + throw new NotImplementedException(); + } + + /// + // This is the version that we need to use for local users. Because reasons. + public Task Authenticate(string username, string password, Data.Entities.User resolvedUser) + { + if (resolvedUser == null) + { + throw new AuthenticationException("Specified user does not exist."); + } + + bool success = false; + + // As long as jellyfin supports passwordless users, we need this little block here to accommodate + if (!HasPassword(resolvedUser)) + { + return Task.FromResult(new ProviderAuthenticationResult + { + Username = username + }); + } + + byte[] passwordBytes = Encoding.UTF8.GetBytes(password); + + PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password); + if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id) + || _cryptographyProvider.DefaultHashMethod == readyHash.Id) + { + byte[] calculatedHash = _cryptographyProvider.ComputeHash( + readyHash.Id, + passwordBytes, + readyHash.Salt.ToArray()); + + if (readyHash.Hash.SequenceEqual(calculatedHash)) + { + success = true; + } + } + else + { + throw new AuthenticationException($"Requested crypto method not available in provider: {readyHash.Id}"); + } + + if (!success) + { + throw new AuthenticationException("Invalid username or password"); + } + + return Task.FromResult(new ProviderAuthenticationResult + { + Username = username + }); + } + + /// + public bool HasPassword(Data.Entities.User user) + => !string.IsNullOrEmpty(user.Password); + + /// + public Task ChangePassword(Data.Entities.User user, string newPassword) + { + if (string.IsNullOrEmpty(newPassword)) + { + user.Password = null; + return Task.CompletedTask; + } + + PasswordHash newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword); + user.Password = newPasswordHash.ToString(); + + return Task.CompletedTask; + } + + /// + public void ChangeEasyPassword(Data.Entities.User user, string newPassword, string newPasswordHash) + { + if (newPassword != null) + { + newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword).ToString(); + } + + if (string.IsNullOrWhiteSpace(newPasswordHash)) + { + throw new ArgumentNullException(nameof(newPasswordHash)); + } + + user.EasyPassword = newPasswordHash; + } + + /// + public string GetEasyPasswordHash(Data.Entities.User user) + { + return string.IsNullOrEmpty(user.EasyPassword) + ? null + : Hex.Encode(PasswordHash.Parse(user.EasyPassword).Hash); + } + + /// + /// Hashes the provided string. + /// + /// The user. + /// The string to hash. + /// The hashed string. + public string GetHashedString(Data.Entities.User user, string str) + { + if (string.IsNullOrEmpty(user.Password)) + { + return _cryptographyProvider.CreatePasswordHash(str).ToString(); + } + + // TODO: make use of iterations parameter? + PasswordHash passwordHash = PasswordHash.Parse(user.Password); + var salt = passwordHash.Salt.ToArray(); + return new PasswordHash( + passwordHash.Id, + _cryptographyProvider.ComputeHash( + passwordHash.Id, + Encoding.UTF8.GetBytes(str), + salt), + salt, + passwordHash.Parameters.ToDictionary(x => x.Key, y => y.Value)).ToString(); + } + + /// + /// Hashes the provided string. + /// + /// The user. + /// The string to hash. + /// The hashed string. + public ReadOnlySpan GetHashed(Data.Entities.User user, string str) + { + if (string.IsNullOrEmpty(user.Password)) + { + return _cryptographyProvider.CreatePasswordHash(str).Hash; + } + + // TODO: make use of iterations parameter? + PasswordHash passwordHash = PasswordHash.Parse(user.Password); + return _cryptographyProvider.ComputeHash( + passwordHash.Id, + Encoding.UTF8.GetBytes(str), + passwordHash.Salt.ToArray()); + } + } +} diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs new file mode 100644 index 000000000..60b48ec76 --- /dev/null +++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Users; + +namespace Jellyfin.Server.Implementations.Users +{ + /// + /// The default password reset provider. + /// + public class DefaultPasswordResetProvider : IPasswordResetProvider + { + private const string BaseResetFileName = "passwordreset"; + + private readonly IJsonSerializer _jsonSerializer; + private readonly IUserManager _userManager; + + private readonly string _passwordResetFileBase; + private readonly string _passwordResetFileBaseDir; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration manager. + /// The JSON serializer. + /// The user manager. + public DefaultPasswordResetProvider( + IServerConfigurationManager configurationManager, + IJsonSerializer jsonSerializer, + IUserManager userManager) + { + _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath; + _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName); + _jsonSerializer = jsonSerializer; + _userManager = userManager; + } + + /// + public string Name => "Default Password Reset Provider"; + + /// + public bool IsEnabled => true; + + /// + public async Task RedeemPasswordResetPin(string pin) + { + SerializablePasswordReset spr; + List usersReset = new List(); + foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*")) + { + await using (var str = File.OpenRead(resetFile)) + { + spr = await _jsonSerializer.DeserializeFromStreamAsync(str).ConfigureAwait(false); + } + + if (spr.ExpirationDate < DateTime.Now) + { + File.Delete(resetFile); + } + else if (string.Equals( + spr.Pin.Replace("-", string.Empty, StringComparison.Ordinal), + pin.Replace("-", string.Empty, StringComparison.Ordinal), + StringComparison.InvariantCultureIgnoreCase)) + { + var resetUser = _userManager.GetUserByName(spr.UserName); + if (resetUser == null) + { + throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found"); + } + + await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); + usersReset.Add(resetUser.Username); + File.Delete(resetFile); + } + } + + if (usersReset.Count < 1) + { + throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}"); + } + + return new PinRedeemResult + { + Success = true, + UsersReset = usersReset.ToArray() + }; + } + + /// + public async Task StartForgotPasswordProcess(User user, bool isInNetwork) + { + string pin; + using (var cryptoRandom = RandomNumberGenerator.Create()) + { + byte[] bytes = new byte[4]; + cryptoRandom.GetBytes(bytes); + pin = BitConverter.ToString(bytes); + } + + DateTime expireTime = DateTime.Now.AddMinutes(30); + + user.EasyPassword = pin; + await _userManager.UpdateUserAsync(user).ConfigureAwait(false); + + return new ForgotPasswordResult + { + Action = ForgotPasswordAction.PinCode, + PinExpirationDate = expireTime, + }; + } + + private class SerializablePasswordReset : PasswordPinCreationResult + { + public string Pin { get; set; } + + public string UserName { get; set; } + } + } +} diff --git a/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs b/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs new file mode 100644 index 000000000..d94a27b9d --- /dev/null +++ b/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs @@ -0,0 +1,66 @@ +#pragma warning disable CS1591 + +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Security; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Events; + +namespace Jellyfin.Server.Implementations.Users +{ + public sealed class DeviceAccessEntryPoint : IServerEntryPoint + { + private readonly IUserManager _userManager; + private readonly IAuthenticationRepository _authRepo; + private readonly IDeviceManager _deviceManager; + private readonly ISessionManager _sessionManager; + + public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager) + { + _userManager = userManager; + _authRepo = authRepo; + _deviceManager = deviceManager; + _sessionManager = sessionManager; + } + + public Task RunAsync() + { + _userManager.OnUserUpdated += OnUserUpdated; + + return Task.CompletedTask; + } + + public void Dispose() + { + } + + private void OnUserUpdated(object sender, GenericEventArgs e) + { + var user = e.Argument; + if (!user.HasPermission(PermissionKind.EnableAllDevices)) + { + UpdateDeviceAccess(user); + } + } + + private void UpdateDeviceAccess(User user) + { + var existing = _authRepo.Get(new AuthenticationInfoQuery + { + UserId = user.Id + }).Items; + + foreach (var authInfo in existing) + { + if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId)) + { + _sessionManager.Logout(authInfo); + } + } + } + } +} diff --git a/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs new file mode 100644 index 000000000..e430808bf --- /dev/null +++ b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using MediaBrowser.Controller.Authentication; + +namespace Jellyfin.Server.Implementations.Users +{ + /// + /// An invalid authentication provider. + /// + public class InvalidAuthProvider : IAuthenticationProvider + { + /// + public string Name => "InvalidOrMissingAuthenticationProvider"; + + /// + public bool IsEnabled => true; + + /// + public Task Authenticate(string username, string password) + { + throw new AuthenticationException("User Account cannot login with this provider. The Normal provider for this user cannot be found"); + } + + /// + public bool HasPassword(Data.Entities.User user) + { + return true; + } + + /// + public Task ChangePassword(Data.Entities.User user, string newPassword) + { + return Task.CompletedTask; + } + + /// + public void ChangeEasyPassword(Data.Entities.User user, string newPassword, string newPasswordHash) + { + // Nothing here + } + + /// + public string GetEasyPasswordHash(Data.Entities.User user) + { + return string.Empty; + } + } +} diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs new file mode 100644 index 000000000..ddc05055b --- /dev/null +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -0,0 +1,763 @@ +#pragma warning disable CA1307 +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using MediaBrowser.Common.Cryptography; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.Users; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Implementations.Users +{ + public class UserManager : IUserManager + { + private readonly JellyfinDbProvider _dbProvider; + private readonly ICryptoProvider _cryptoProvider; + private readonly INetworkManager _networkManager; + private readonly ILogger _logger; + + private IAuthenticationProvider[] _authenticationProviders; + private DefaultAuthenticationProvider _defaultAuthenticationProvider; + private InvalidAuthProvider _invalidAuthProvider; + private IPasswordResetProvider[] _passwordResetProviders; + private DefaultPasswordResetProvider _defaultPasswordResetProvider; + + public UserManager( + JellyfinDbProvider dbProvider, + ICryptoProvider cryptoProvider, + INetworkManager networkManager, + ILogger logger) + { + _dbProvider = dbProvider; + _cryptoProvider = cryptoProvider; + _networkManager = networkManager; + _logger = logger; + } + + public event EventHandler> OnUserPasswordChanged; + + /// + public event EventHandler> OnUserUpdated; + + /// + public event EventHandler> OnUserCreated; + + /// + public event EventHandler> OnUserDeleted; + + public event EventHandler> OnUserLockedOut; + + public IEnumerable Users + { + get + { + var dbContext = _dbProvider.CreateContext(); + return dbContext.Users; + } + } + + public IEnumerable UsersIds + { + get + { + var dbContext = _dbProvider.CreateContext(); + return dbContext.Users.Select(u => u.Id); + } + } + + public User GetUserById(Guid id) + { + if (id == Guid.Empty) + { + throw new ArgumentException("Guid can't be empty", nameof(id)); + } + + var dbContext = _dbProvider.CreateContext(); + + return dbContext.Users.Find(id); + } + + public User GetUserByName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Invalid username", nameof(name)); + } + + var dbContext = _dbProvider.CreateContext(); + + // This can't use an overload with StringComparer because that would cause the query to + // have to be evaluated client-side. + return dbContext.Users.FirstOrDefault(u => string.Equals(u.Username, name)); + } + + public async Task RenameUser(User user, string newName) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (string.IsNullOrWhiteSpace(newName)) + { + throw new ArgumentException("Invalid username", nameof(newName)); + } + + if (user.Username.Equals(newName, StringComparison.Ordinal)) + { + throw new ArgumentException("The new and old names must be different."); + } + + if (Users.Any( + u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException(string.Format( + CultureInfo.InvariantCulture, + "A user with the name '{0}' already exists.", + newName)); + } + + user.Username = newName; + await UpdateUserAsync(user).ConfigureAwait(false); + + OnUserUpdated?.Invoke(this, new GenericEventArgs(user)); + } + + public void UpdateUser(User user) + { + var dbContext = _dbProvider.CreateContext(); + dbContext.Users.Update(user); + dbContext.SaveChanges(); + } + + public async Task UpdateUserAsync(User user) + { + var dbContext = _dbProvider.CreateContext(); + dbContext.Users.Update(user); + + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + + public User CreateUser(string name) + { + if (!IsValidUsername(name)) + { + throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)"); + } + + var dbContext = _dbProvider.CreateContext(); + + var newUser = new User(name, _defaultAuthenticationProvider.GetType().FullName); + dbContext.Users.Add(newUser); + dbContext.SaveChanges(); + + OnUserCreated?.Invoke(this, new GenericEventArgs(newUser)); + + return newUser; + } + + public void DeleteUser(User user) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + var dbContext = _dbProvider.CreateContext(); + + if (!dbContext.Users.Contains(user)) + { + throw new ArgumentException(string.Format( + CultureInfo.InvariantCulture, + "The user cannot be deleted because there is no user with the Name {0} and Id {1}.", + user.Username, + user.Id)); + } + + if (dbContext.Users.Count() == 1) + { + throw new InvalidOperationException(string.Format( + CultureInfo.InvariantCulture, + "The user '{0}' cannot be deleted because there must be at least one user in the system.", + user.Username)); + } + + if (user.HasPermission(PermissionKind.IsAdministrator) + && Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1) + { + throw new ArgumentException( + string.Format( + CultureInfo.InvariantCulture, + "The user '{0}' cannot be deleted because there must be at least one admin user in the system.", + user.Username), + nameof(user)); + } + + dbContext.Users.Remove(user); + dbContext.SaveChanges(); + OnUserDeleted?.Invoke(this, new GenericEventArgs(user)); + } + + public Task ResetPassword(User user) + { + return ChangePassword(user, string.Empty); + } + + public void ResetEasyPassword(User user) + { + ChangeEasyPassword(user, string.Empty, null); + } + + public async Task ChangePassword(User user, string newPassword) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false); + await UpdateUserAsync(user).ConfigureAwait(false); + + OnUserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); + } + + public void ChangeEasyPassword(User user, string newPassword, string newPasswordSha1) + { + GetAuthenticationProvider(user).ChangeEasyPassword(user, newPassword, newPasswordSha1); + UpdateUser(user); + + OnUserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); + } + + public UserDto GetUserDto(User user, string remoteEndPoint = null) + { + return new UserDto + { + Id = user.Id, + HasPassword = user.Password == null, + EnableAutoLogin = user.EnableAutoLogin, + LastLoginDate = user.LastLoginDate, + LastActivityDate = user.LastActivityDate, + Configuration = new UserConfiguration + { + SubtitleMode = user.SubtitleMode, + HidePlayedInLatest = user.HidePlayedInLatest, + EnableLocalPassword = user.EnableLocalPassword, + PlayDefaultAudioTrack = user.PlayDefaultAudioTrack, + DisplayCollectionsView = user.DisplayCollectionsView, + DisplayMissingEpisodes = user.DisplayMissingEpisodes, + AudioLanguagePreference = user.AudioLanguagePreference, + RememberAudioSelections = user.RememberAudioSelections, + EnableNextEpisodeAutoPlay = user.EnableNextEpisodeAutoPlay, + RememberSubtitleSelections = user.RememberSubtitleSelections, + SubtitleLanguagePreference = user.SubtitleLanguagePreference, + OrderedViews = user.GetPreference(PreferenceKind.OrderedViews), + GroupedFolders = user.GetPreference(PreferenceKind.GroupedFolders), + MyMediaExcludes = user.GetPreference(PreferenceKind.MyMediaExcludes), + LatestItemsExcludes = user.GetPreference(PreferenceKind.LatestItemExcludes) + }, + Policy = new UserPolicy + { + MaxParentalRating = user.MaxParentalAgeRating, + EnableUserPreferenceAccess = user.EnableUserPreferenceAccess, + RemoteClientBitrateLimit = user.RemoteClientBitrateLimit.GetValueOrDefault(), + AuthenticationProviderId = user.AuthenticationProviderId, + PasswordResetProviderId = user.PasswordResetProviderId, + InvalidLoginAttemptCount = user.InvalidLoginAttemptCount, + LoginAttemptsBeforeLockout = user.LoginAttemptsBeforeLockout.GetValueOrDefault(), + IsAdministrator = user.HasPermission(PermissionKind.IsAdministrator), + IsHidden = user.HasPermission(PermissionKind.IsHidden), + IsDisabled = user.HasPermission(PermissionKind.IsDisabled), + EnableSharedDeviceControl = user.HasPermission(PermissionKind.EnableSharedDeviceControl), + EnableRemoteAccess = user.HasPermission(PermissionKind.EnableRemoteAccess), + EnableLiveTvManagement = user.HasPermission(PermissionKind.EnableLiveTvManagement), + EnableLiveTvAccess = user.HasPermission(PermissionKind.EnableLiveTvAccess), + EnableMediaPlayback = user.HasPermission(PermissionKind.EnableMediaPlayback), + EnableAudioPlaybackTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding), + EnableVideoPlaybackTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding), + EnableContentDeletion = user.HasPermission(PermissionKind.EnableContentDeletion), + EnableContentDownloading = user.HasPermission(PermissionKind.EnableContentDownloading), + EnableSyncTranscoding = user.HasPermission(PermissionKind.EnableSyncTranscoding), + EnableMediaConversion = user.HasPermission(PermissionKind.EnableMediaConversion), + EnableAllChannels = user.HasPermission(PermissionKind.EnableAllChannels), + EnableAllDevices = user.HasPermission(PermissionKind.EnableAllDevices), + EnableAllFolders = user.HasPermission(PermissionKind.EnableAllFolders), + EnableRemoteControlOfOtherUsers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers), + EnablePlaybackRemuxing = user.HasPermission(PermissionKind.EnablePlaybackRemuxing), + ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding), + EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing), + AccessSchedules = user.AccessSchedules.ToArray(), + BlockedTags = user.GetPreference(PreferenceKind.BlockedTags), + EnabledChannels = user.GetPreference(PreferenceKind.EnabledChannels), + EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices), + EnabledFolders = user.GetPreference(PreferenceKind.EnabledFolders), + EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders) + } + }; + } + + public PublicUserDto GetPublicUserDto(User user, string remoteEndPoint = null) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user); + bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(GetAuthenticationProvider(user).GetEasyPasswordHash(user)); + + bool hasPassword = user.EnableLocalPassword && + !string.IsNullOrEmpty(remoteEndPoint) && + _networkManager.IsInLocalNetwork(remoteEndPoint) ? hasConfiguredEasyPassword : hasConfiguredPassword; + + return new PublicUserDto + { + Name = user.Username, + HasPassword = hasPassword, + HasConfiguredPassword = hasConfiguredPassword + }; + } + + public async Task AuthenticateUser( + string username, + string password, + string passwordSha1, + string remoteEndPoint, + bool isUserSession) + { + if (string.IsNullOrWhiteSpace(username)) + { + _logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndPoint); + throw new ArgumentNullException(nameof(username)); + } + + var user = Users.ToList().FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); + bool success; + IAuthenticationProvider authenticationProvider; + + if (user != null) + { + var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint) + .ConfigureAwait(false); + authenticationProvider = authResult.authenticationProvider; + success = authResult.success; + } + else + { + var authResult = await AuthenticateLocalUser(username, password, null, remoteEndPoint) + .ConfigureAwait(false); + authenticationProvider = authResult.authenticationProvider; + string updatedUsername = authResult.username; + success = authResult.success; + + if (success + && authenticationProvider != null + && !(authenticationProvider is DefaultAuthenticationProvider)) + { + // Trust the username returned by the authentication provider + username = updatedUsername; + + // Search the database for the user again + // the authentication provider might have created it + user = Users + .ToList().FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); + + if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy) + { + UpdatePolicy(user.Id, hasNewUserPolicy.GetNewUserPolicy()); + + await UpdateUserAsync(user).ConfigureAwait(false); + } + } + } + + if (success && user != null && authenticationProvider != null) + { + var providerId = authenticationProvider.GetType().FullName; + + if (!string.Equals(providerId, user.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase)) + { + user.AuthenticationProviderId = providerId; + await UpdateUserAsync(user).ConfigureAwait(false); + } + } + + if (user == null) + { + _logger.LogInformation( + "Authentication request for {UserName} has been denied (IP: {IP}).", + username, + remoteEndPoint); + throw new AuthenticationException("Invalid username or password entered."); + } + + if (user.HasPermission(PermissionKind.IsDisabled)) + { + _logger.LogInformation( + "Authentication request for {UserName} has been denied because this account is currently disabled (IP: {IP}).", + username, + remoteEndPoint); + throw new SecurityException( + $"The {user.Username} account is currently disabled. Please consult with your administrator."); + } + + if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && + !_networkManager.IsInLocalNetwork(remoteEndPoint)) + { + _logger.LogInformation( + "Authentication request for {UserName} forbidden: remote access disabled and user not in local network (IP: {IP}).", + username, + remoteEndPoint); + throw new SecurityException("Forbidden."); + } + + if (!user.IsParentalScheduleAllowed()) + { + _logger.LogInformation( + "Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {IP}).", + username, + remoteEndPoint); + throw new SecurityException("User is not allowed access at this time."); + } + + // Update LastActivityDate and LastLoginDate, then save + if (success) + { + if (isUserSession) + { + user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; + await UpdateUserAsync(user).ConfigureAwait(false); + } + + user.InvalidLoginAttemptCount = 0; + _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Username); + } + else + { + IncrementInvalidLoginAttemptCount(user); + _logger.LogInformation( + "Authentication request for {UserName} has been denied (IP: {IP}).", + user.Username, + remoteEndPoint); + } + + return success ? user : null; + } + + public async Task StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) + { + var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername); + + var action = ForgotPasswordAction.InNetworkRequired; + + if (user != null && isInNetwork) + { + var passwordResetProvider = GetPasswordResetProvider(user); + return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false); + } + + return new ForgotPasswordResult + { + Action = action, + PinFile = string.Empty + }; + } + + public async Task RedeemPasswordResetPin(string pin) + { + foreach (var provider in _passwordResetProviders) + { + var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false); + + if (result.Success) + { + return result; + } + } + + return new PinRedeemResult + { + Success = false, + UsersReset = Array.Empty() + }; + } + + public void AddParts(IEnumerable authenticationProviders, IEnumerable passwordResetProviders) + { + _authenticationProviders = authenticationProviders.ToArray(); + _passwordResetProviders = passwordResetProviders.ToArray(); + + _invalidAuthProvider = _authenticationProviders.OfType().First(); + _defaultAuthenticationProvider = _authenticationProviders.OfType().First(); + _defaultPasswordResetProvider = _passwordResetProviders.OfType().First(); + } + + public NameIdPair[] GetAuthenticationProviders() + { + return _authenticationProviders + .Where(provider => provider.IsEnabled) + .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1) + .ThenBy(i => i.Name) + .Select(i => new NameIdPair + { + Name = i.Name, + Id = i.GetType().FullName + }) + .ToArray(); + } + + public NameIdPair[] GetPasswordResetProviders() + { + return _passwordResetProviders + .Where(provider => provider.IsEnabled) + .OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1) + .ThenBy(i => i.Name) + .Select(i => new NameIdPair + { + Name = i.Name, + Id = i.GetType().FullName + }) + .ToArray(); + } + + public void UpdateConfiguration(Guid userId, UserConfiguration config) + { + var user = GetUserById(userId); + user.SubtitleMode = config.SubtitleMode; + user.HidePlayedInLatest = config.HidePlayedInLatest; + user.EnableLocalPassword = config.EnableLocalPassword; + user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack; + user.DisplayCollectionsView = config.DisplayCollectionsView; + user.DisplayMissingEpisodes = config.DisplayMissingEpisodes; + user.AudioLanguagePreference = config.AudioLanguagePreference; + user.RememberAudioSelections = config.RememberAudioSelections; + user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay; + user.RememberSubtitleSelections = config.RememberSubtitleSelections; + user.SubtitleLanguagePreference = config.SubtitleLanguagePreference; + + user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews); + user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders); + user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes); + user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes); + + UpdateUser(user); + } + + public void UpdatePolicy(Guid userId, UserPolicy policy) + { + var user = GetUserById(userId); + + user.MaxParentalAgeRating = policy.MaxParentalRating; + user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess; + user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit; + user.AuthenticationProviderId = policy.AuthenticationProviderId; + user.PasswordResetProviderId = policy.PasswordResetProviderId; + user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount; + user.LoginAttemptsBeforeLockout = policy.LoginAttemptsBeforeLockout == -1 + ? null + : new int?(policy.LoginAttemptsBeforeLockout); + user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator); + user.SetPermission(PermissionKind.IsHidden, policy.IsHidden); + user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled); + user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl); + user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess); + user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement); + user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess); + user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback); + user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding); + user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding); + user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion); + user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading); + user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding); + user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion); + user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels); + user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices); + user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders); + user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers); + user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing); + user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding); + user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing); + + user.AccessSchedules.Clear(); + foreach (var policyAccessSchedule in policy.AccessSchedules) + { + user.AccessSchedules.Add(policyAccessSchedule); + } + + user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags); + user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels); + user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices); + user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders); + user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders); + } + + private bool IsValidUsername(string name) + { + // This is some regex that matches only on unicode "word" characters, as well as -, _ and @ + // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness + // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.) + return Regex.IsMatch(name, @"^[\w\-'._@]*$"); + } + + private IAuthenticationProvider GetAuthenticationProvider(User user) + { + return GetAuthenticationProviders(user)[0]; + } + + private IPasswordResetProvider GetPasswordResetProvider(User user) + { + return GetPasswordResetProviders(user)[0]; + } + + private IList GetAuthenticationProviders(User user) + { + var authenticationProviderId = user?.AuthenticationProviderId; + + var providers = _authenticationProviders.Where(i => i.IsEnabled).ToList(); + + if (!string.IsNullOrEmpty(authenticationProviderId)) + { + providers = providers.Where(i => string.Equals(authenticationProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase)).ToList(); + } + + if (providers.Count == 0) + { + // Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found + _logger.LogWarning( + "User {Username} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. Assigning user to InvalidAuthProvider until this is corrected", + user?.Username, + user?.AuthenticationProviderId); + providers = new List + { + _invalidAuthProvider + }; + } + + return providers; + } + + private IList GetPasswordResetProviders(User user) + { + var passwordResetProviderId = user?.PasswordResetProviderId; + var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray(); + + if (!string.IsNullOrEmpty(passwordResetProviderId)) + { + providers = providers.Where(i => + string.Equals(passwordResetProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + } + + if (providers.Length == 0) + { + providers = new IPasswordResetProvider[] + { + _defaultPasswordResetProvider + }; + } + + return providers; + } + + private async Task<(IAuthenticationProvider authenticationProvider, string username, bool success)> AuthenticateLocalUser( + string username, + string password, + User user, + string remoteEndPoint) + { + bool success = false; + IAuthenticationProvider authenticationProvider = null; + + foreach (var provider in GetAuthenticationProviders(user)) + { + var providerAuthResult = + await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); + var updatedUsername = providerAuthResult.username; + success = providerAuthResult.success; + + if (success) + { + authenticationProvider = provider; + username = updatedUsername; + break; + } + } + + if (!success + && _networkManager.IsInLocalNetwork(remoteEndPoint) + && user?.EnableLocalPassword == true + && !string.IsNullOrEmpty(user.EasyPassword)) + { + // Check easy password + var passwordHash = PasswordHash.Parse(user.EasyPassword); + var hash = _cryptoProvider.ComputeHash( + passwordHash.Id, + Encoding.UTF8.GetBytes(password), + passwordHash.Salt.ToArray()); + success = passwordHash.Hash.SequenceEqual(hash); + } + + return (authenticationProvider, username, success); + } + + private async Task<(string username, bool success)> AuthenticateWithProvider( + IAuthenticationProvider provider, + string username, + string password, + User resolvedUser) + { + try + { + var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser + ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false) + : await provider.Authenticate(username, password).ConfigureAwait(false); + + if (authenticationResult.Username != username) + { + _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username); + username = authenticationResult.Username; + } + + return (username, true); + } + catch (AuthenticationException ex) + { + _logger.LogError(ex, "Error authenticating with provider {Provider}", provider.Name); + + return (username, false); + } + } + + private void IncrementInvalidLoginAttemptCount(User user) + { + int invalidLogins = user.InvalidLoginAttemptCount; + int? maxInvalidLogins = user.LoginAttemptsBeforeLockout; + if (maxInvalidLogins.HasValue && invalidLogins >= maxInvalidLogins) + { + user.SetPermission(PermissionKind.IsDisabled, true); + OnUserLockedOut?.Invoke(this, new GenericEventArgs(user)); + _logger.LogWarning( + "Disabling user {Username} due to {Attempts} unsuccessful login attempts.", + user.Username, + invalidLogins); + } + + UpdateUser(user); + } + } +} diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 331a32c73..b4c5fd4ea 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -7,8 +7,10 @@ using Emby.Server.Implementations; using Jellyfin.Drawing.Skia; using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Activity; +using Jellyfin.Server.Implementations.Users; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Library; using MediaBrowser.Model.Activity; using MediaBrowser.Model.IO; using Microsoft.EntityFrameworkCore; @@ -69,6 +71,7 @@ namespace Jellyfin.Server serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); base.RegisterServices(serviceCollection); } @@ -80,6 +83,9 @@ namespace Jellyfin.Server protected override IEnumerable GetAssembliesWithPartsInternal() { yield return typeof(CoreAppHost).Assembly; + yield return typeof(DefaultAuthenticationProvider).Assembly; + yield return typeof(DefaultPasswordResetProvider).Assembly; + yield return typeof(InvalidAuthProvider).Assembly; } /// diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index ea16c5573..9eec6ed4e 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -41,7 +41,6 @@ - diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index b19d7f7fc..0271098f4 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -1,33 +1,45 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; +using System; using System.IO; using Emby.Server.Implementations.Data; using Emby.Server.Implementations.Serialization; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Server.Implementations; +using Jellyfin.Server.Implementations.Users; using MediaBrowser.Controller; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Users; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; using SQLitePCL.pretty; +using JsonSerializer = System.Text.Json.JsonSerializer; namespace Jellyfin.Server.Migrations.Routines { + /// + /// The migration routine for migrating the user database to EF Core. + /// public class MigrateUserDb : IMigrationRoutine { - private readonly ILogger _logger; + private const string DbFilename = "users.db"; + private readonly ILogger _logger; private readonly IServerApplicationPaths _paths; - private readonly JellyfinDbProvider _provider; - private readonly MyXmlSerializer _xmlSerializer; - public MigrateUserDb(ILogger logger, IServerApplicationPaths paths, JellyfinDbProvider provider, MyXmlSerializer xmlSerializer) + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The server application paths. + /// The database provider. + /// The xml serializer. + public MigrateUserDb( + ILogger logger, + IServerApplicationPaths paths, + JellyfinDbProvider provider, + MyXmlSerializer xmlSerializer) { _logger = logger; _paths = paths; @@ -35,18 +47,21 @@ namespace Jellyfin.Server.Migrations.Routines _xmlSerializer = xmlSerializer; } + /// public Guid Id => Guid.Parse("5C4B82A2-F053-4009-BD05-B6FCAD82F14C"); - public string Name => "MigrateUserDb"; + /// + public string Name => "MigrateUserDatabase"; + /// public void Perform() { var dataPath = _paths.DataPath; _logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin."); - using (var connection = SQLite3.Open(Path.Combine(dataPath, "users.db"), ConnectionFlags.ReadOnly, null)) + using (var connection = SQLite3.Open(Path.Combine(dataPath, DbFilename), ConnectionFlags.ReadOnly, null)) { - using var dbContext = _provider.CreateContext(); + var dbContext = _provider.CreateContext(); var queryResult = connection.Query("SELECT * FROM LocalUsersv2"); @@ -55,26 +70,30 @@ namespace Jellyfin.Server.Migrations.Routines foreach (var entry in queryResult) { - var json = JsonConvert.DeserializeObject>(entry[2].ToString()); - var userDataDir = Path.Combine(_paths.UserConfigurationDirectoryPath, json["Name"]); - - var config = (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), Path.Combine(userDataDir, "config.xml")); - var policy = (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), Path.Combine(userDataDir, "policy.xml")); - - var user = new User( - json["Name"], - false, - policy.AuthenticatioIsnProviderId, - policy.InvalidLoginAttemptCount, - config.SubtitleMode, - config.PlayDefaultAudioTrack) + UserMockup mockup = JsonSerializer.Deserialize(entry[2].ToBlob()); + var userDataDir = Path.Combine(_paths.UserConfigurationDirectoryPath, mockup.Name); + + var config = File.Exists(Path.Combine(userDataDir, "config.xml")) + ? (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), Path.Combine(userDataDir, "config.xml")) + : new UserConfiguration(); + var policy = File.Exists(Path.Combine(userDataDir, "policy.xml")) + ? (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), Path.Combine(userDataDir, "policy.xml")) + : new UserPolicy(); + policy.AuthenticationProviderId = policy.AuthenticationProviderId?.Replace( + "Emby.Server.Implementations.Library", + "Jellyfin.Server.Implementations.Users", + StringComparison.Ordinal) + ?? typeof(DefaultAuthenticationProvider).FullName; + + policy.PasswordResetProviderId ??= typeof(DefaultPasswordResetProvider).FullName; + + var user = new User(mockup.Name, policy.AuthenticationProviderId) { Id = entry[1].ReadGuidFromBlob(), InternalId = entry[0].ToInt64(), MaxParentalAgeRating = policy.MaxParentalRating, EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess, RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit, - AuthenticationProviderId = policy.AuthenticatioIsnProviderId, PasswordResetProviderId = policy.PasswordResetProviderId, InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount, LoginAttemptsBeforeLockout = policy.LoginAttemptsBeforeLockout == -1 ? null : new int?(policy.LoginAttemptsBeforeLockout), @@ -89,6 +108,10 @@ namespace Jellyfin.Server.Migrations.Routines EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay, RememberSubtitleSelections = config.RememberSubtitleSelections, SubtitleLanguagePreference = config.SubtitleLanguagePreference, + Password = mockup.Password, + EasyPassword = mockup.EasyPassword, + LastLoginDate = mockup.LastLoginDate ?? DateTime.MinValue, + LastActivityDate = mockup.LastActivityDate ?? DateTime.MinValue }; user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator); @@ -112,6 +135,7 @@ namespace Jellyfin.Server.Migrations.Routines user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing); user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding); user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing); + foreach (var policyAccessSchedule in policy.AccessSchedules) { user.AccessSchedules.Add(policyAccessSchedule); @@ -126,6 +150,8 @@ namespace Jellyfin.Server.Migrations.Routines user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders); user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes); user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes); + + dbContext.Users.Add(user); } dbContext.SaveChanges(); @@ -133,12 +159,32 @@ namespace Jellyfin.Server.Migrations.Routines try { - File.Move(Path.Combine(dataPath, "users.db"), Path.Combine(dataPath, "users.db" + ".old")); + File.Move(Path.Combine(dataPath, DbFilename), Path.Combine(dataPath, DbFilename + ".old")); + + var journalPath = Path.Combine(dataPath, DbFilename + "-journal"); + if (File.Exists(journalPath)) + { + File.Move(journalPath, Path.Combine(dataPath, DbFilename + ".old-journal")); + } } catch (IOException e) { _logger.LogError(e, "Error renaming legacy user database to 'users.db.old'"); } } + +#nullable disable + internal class UserMockup + { + public string Password { get; set; } + + public string EasyPassword { get; set; } + + public DateTime? LastLoginDate { get; set; } + + public DateTime? LastActivityDate { get; set; } + + public string Name { get; set; } + } } } diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 8d94d3971..bc9724027 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -1,6 +1,8 @@ #pragma warning disable CS1591 using System; +using System.Text.Json.Serialization; +using System.Xml.Serialization; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; @@ -34,7 +36,7 @@ namespace MediaBrowser.Model.Users public string[] BlockedTags { get; set; } public bool EnableUserPreferenceAccess { get; set; } - public Jellyfin.Data.Entities.AccessSchedule[] AccessSchedules { get; set; } + public AccessSchedule[] AccessSchedules { get; set; } public UnratedItem[] BlockUnratedItems { get; set; } public bool EnableRemoteControlOfOtherUsers { get; set; } public bool EnableSharedDeviceControl { get; set; } @@ -78,7 +80,9 @@ namespace MediaBrowser.Model.Users public string[] BlockedChannels { get; set; } public int RemoteClientBitrateLimit { get; set; } - public string AuthenticatioIsnProviderId { get; set; } + + [XmlElement(ElementName = "AuthenticationProviderId")] + public string AuthenticationProviderId { get; set; } public string PasswordResetProviderId { get; set; } public UserPolicy() -- cgit v1.2.3 From 44957c5a9a11fcd6e4567c7d31bc39d79c709068 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 5 Jun 2020 18:15:56 -0600 Subject: Use typed logger where possible --- Emby.Dlna/ConnectionManager/ConnectionManager.cs | 2 +- Emby.Dlna/DlnaManager.cs | 4 ++-- Emby.Dlna/Main/DlnaEntryPoint.cs | 10 ++++++---- Emby.Dlna/PlayTo/PlayToManager.cs | 3 ++- Emby.Dlna/Service/BaseService.cs | 4 ++-- Emby.Drawing/ImageProcessor.cs | 2 +- Emby.Notifications/NotificationEntryPoint.cs | 2 +- Emby.Notifications/NotificationManager.cs | 2 +- Emby.Photos/PhotoProvider.cs | 2 +- .../AppBase/BaseConfigurationManager.cs | 4 ++-- Emby.Server.Implementations/ApplicationHost.cs | 2 +- Emby.Server.Implementations/Browser/BrowserLauncher.cs | 2 +- Emby.Server.Implementations/Channels/ChannelManager.cs | 2 +- .../Channels/RefreshChannelsScheduledTask.cs | 2 +- Emby.Server.Implementations/Collections/CollectionManager.cs | 6 +++--- Emby.Server.Implementations/Data/BaseSqliteRepository.cs | 4 ++-- Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs | 2 +- Emby.Server.Implementations/Devices/DeviceId.cs | 4 ++-- Emby.Server.Implementations/Dto/DtoService.cs | 2 +- .../EntryPoints/ExternalPortForwarding.cs | 2 +- .../EntryPoints/LibraryChangedNotifier.cs | 2 +- Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs | 2 +- Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs | 2 +- .../HttpClientManager/HttpClientManager.cs | 2 +- Emby.Server.Implementations/HttpServer/HttpListenerHost.cs | 4 ++-- Emby.Server.Implementations/HttpServer/HttpResultFactory.cs | 4 ++-- Emby.Server.Implementations/HttpServer/Security/AuthService.cs | 2 +- Emby.Server.Implementations/HttpServer/WebSocketConnection.cs | 2 +- Emby.Server.Implementations/IO/LibraryMonitor.cs | 2 +- Emby.Server.Implementations/IO/ManagedFileSystem.cs | 2 +- Emby.Server.Implementations/Library/LibraryManager.cs | 2 +- Emby.Server.Implementations/Library/MediaSourceManager.cs | 2 +- .../Library/Resolvers/Audio/MusicAlbumResolver.cs | 2 +- .../Library/Resolvers/TV/SeasonResolver.cs | 2 +- .../Library/Resolvers/TV/SeriesResolver.cs | 4 ++-- Emby.Server.Implementations/Library/SearchEngine.cs | 2 +- Emby.Server.Implementations/Library/UserDataManager.cs | 2 +- Emby.Server.Implementations/Library/UserManager.cs | 2 +- .../Library/Validators/ArtistsPostScanTask.cs | 4 ++-- .../Library/Validators/ArtistsValidator.cs | 4 ++-- .../Library/Validators/GenresPostScanTask.cs | 4 ++-- .../Library/Validators/GenresValidator.cs | 4 ++-- .../Library/Validators/MusicGenresPostScanTask.cs | 4 ++-- .../Library/Validators/MusicGenresValidator.cs | 4 ++-- .../Library/Validators/StudiosPostScanTask.cs | 4 ++-- .../Library/Validators/StudiosValidator.cs | 4 ++-- Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs | 2 +- Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs | 2 +- .../LiveTv/Listings/XmlTvListingsProvider.cs | 2 +- Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs | 2 +- Emby.Server.Implementations/LiveTv/LiveTvManager.cs | 2 +- .../LiveTv/LiveTvMediaSourceProvider.cs | 2 +- Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs | 4 ++-- Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs | 1 + .../Localization/LocalizationManager.cs | 2 +- Emby.Server.Implementations/MediaEncoder/EncodingManager.cs | 2 +- Emby.Server.Implementations/Networking/NetworkManager.cs | 2 +- Emby.Server.Implementations/Playlists/PlaylistManager.cs | 2 +- Emby.Server.Implementations/ResourceFileManager.cs | 2 +- Emby.Server.Implementations/ScheduledTasks/TaskManager.cs | 2 +- .../ScheduledTasks/Tasks/ChapterImagesTask.cs | 4 ++-- .../ScheduledTasks/Tasks/DeleteCacheFileTask.cs | 2 +- .../ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs | 2 +- .../ScheduledTasks/Tasks/PluginUpdateTask.cs | 2 +- Emby.Server.Implementations/Services/ServiceController.cs | 2 +- Emby.Server.Implementations/Session/SessionManager.cs | 2 +- .../Session/SessionWebSocketListener.cs | 2 +- Emby.Server.Implementations/Session/WebSocketController.cs | 2 +- .../SocketSharp/WebSocketSharpRequest.cs | 2 +- Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs | 2 +- Emby.Server.Implementations/Updates/InstallationManager.cs | 2 +- Jellyfin.Drawing.Skia/SkiaEncoder.cs | 2 +- .../Migrations/Routines/DisableTranscodingThrottling.cs | 2 +- Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs | 2 +- MediaBrowser.Api/ApiEntryPoint.cs | 2 +- MediaBrowser.Api/Music/AlbumsService.cs | 10 ++++++---- MediaBrowser.Api/SimilarItemsHelper.cs | 2 +- MediaBrowser.Controller/Entities/BaseItem.cs | 3 ++- MediaBrowser.Controller/Entities/UserView.cs | 3 ++- MediaBrowser.Controller/Entities/UserViewBuilder.cs | 4 ++-- MediaBrowser.Controller/IO/FileData.cs | 3 ++- MediaBrowser.Controller/Library/Profiler.cs | 2 +- MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs | 4 ++-- MediaBrowser.Controller/Session/SessionInfo.cs | 1 + .../Images/InternalMetadataFolderImageProvider.cs | 2 +- MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs | 4 ++-- MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs | 2 +- MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs | 2 +- MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs | 4 ++-- MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs | 4 ++-- MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs | 4 ++-- MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs | 2 +- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 2 +- MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs | 2 +- MediaBrowser.Providers/Manager/ImageSaver.cs | 1 + MediaBrowser.Providers/Manager/MetadataService.cs | 4 ++-- MediaBrowser.Providers/Manager/ProviderManager.cs | 2 +- MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs | 2 +- MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs | 3 ++- MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs | 2 +- MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs | 2 +- MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs | 2 +- MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs | 2 +- .../Plugins/TheTvdb/TvdbEpisodeImageProvider.cs | 2 +- MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs | 2 +- .../Plugins/TheTvdb/TvdbPersonImageProvider.cs | 2 +- .../Plugins/TheTvdb/TvdbSeasonImageProvider.cs | 2 +- .../Plugins/TheTvdb/TvdbSeriesImageProvider.cs | 2 +- MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs | 2 +- MediaBrowser.Providers/Subtitles/SubtitleManager.cs | 2 +- MediaBrowser.Providers/TV/SeriesMetadataService.cs | 3 ++- MediaBrowser.Providers/Tmdb/BoxSets/TmdbBoxSetProvider.cs | 2 +- MediaBrowser.Providers/Tmdb/Movies/TmdbMovieProvider.cs | 2 +- MediaBrowser.Providers/Tmdb/People/TmdbPersonProvider.cs | 2 +- MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeImageProvider.cs | 4 ++-- MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeProvider.cs | 4 ++-- MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeProviderBase.cs | 4 ++-- MediaBrowser.Providers/Tmdb/TV/TmdbSeasonProvider.cs | 6 +++--- MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs | 2 +- MediaBrowser.WebDashboard/Api/DashboardService.cs | 2 +- MediaBrowser.XbmcMetadata/EntryPoint.cs | 2 +- MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs | 2 +- MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs | 2 +- MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs | 4 ++-- MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs | 2 +- MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs | 2 +- MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs | 2 +- MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs | 6 +++--- 128 files changed, 179 insertions(+), 166 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Dlna/ConnectionManager/ConnectionManager.cs b/Emby.Dlna/ConnectionManager/ConnectionManager.cs index e32cc11bf..480dd3a79 100644 --- a/Emby.Dlna/ConnectionManager/ConnectionManager.cs +++ b/Emby.Dlna/ConnectionManager/ConnectionManager.cs @@ -18,7 +18,7 @@ namespace Emby.Dlna.ConnectionManager public ConnectionManager( IDlnaManager dlna, IServerConfigurationManager config, - ILogger logger, + ILogger logger, IHttpClient httpClient) : base(logger, httpClient) { diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index 10f881fe7..a85e5c35e 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -31,7 +31,7 @@ namespace Emby.Dlna private readonly IApplicationPaths _appPaths; private readonly IXmlSerializer _xmlSerializer; private readonly IFileSystem _fileSystem; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IJsonSerializer _jsonSerializer; private readonly IServerApplicationHost _appHost; private static readonly Assembly _assembly = typeof(DlnaManager).Assembly; @@ -49,7 +49,7 @@ namespace Emby.Dlna _xmlSerializer = xmlSerializer; _fileSystem = fileSystem; _appPaths = appPaths; - _logger = loggerFactory.CreateLogger("Dlna"); + _logger = loggerFactory.CreateLogger(); _jsonSerializer = jsonSerializer; _appHost = appHost; } diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index bcab4adba..47b235e59 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -33,7 +33,7 @@ namespace Emby.Dlna.Main public class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup { private readonly IServerConfigurationManager _config; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IServerApplicationHost _appHost; private PlayToManager _manager; @@ -65,7 +65,8 @@ namespace Emby.Dlna.Main public static DlnaEntryPoint Current; - public DlnaEntryPoint(IServerConfigurationManager config, + public DlnaEntryPoint( + IServerConfigurationManager config, ILoggerFactory loggerFactory, IServerApplicationHost appHost, ISessionManager sessionManager, @@ -99,7 +100,7 @@ namespace Emby.Dlna.Main _mediaEncoder = mediaEncoder; _socketFactory = socketFactory; _networkManager = networkManager; - _logger = loggerFactory.CreateLogger("Dlna"); + _logger = loggerFactory.CreateLogger(); ContentDirectory = new ContentDirectory.ContentDirectory( dlnaManager, @@ -347,7 +348,8 @@ namespace Emby.Dlna.Main try { - _manager = new PlayToManager(_logger, + _manager = new PlayToManager( + _logger, _sessionManager, _libraryManager, _userManager, diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs index bbedd1485..9b0339e5d 100644 --- a/Emby.Dlna/PlayTo/PlayToManager.cs +++ b/Emby.Dlna/PlayTo/PlayToManager.cs @@ -184,7 +184,8 @@ namespace Emby.Dlna.PlayTo serverAddress = _appHost.GetLocalApiUrl(info.LocalIpAddress); } - controller = new PlayToController(sessionInfo, + controller = new PlayToController( + sessionInfo, _sessionManager, _libraryManager, _logger, diff --git a/Emby.Dlna/Service/BaseService.cs b/Emby.Dlna/Service/BaseService.cs index 3704bedcd..4ecffa293 100644 --- a/Emby.Dlna/Service/BaseService.cs +++ b/Emby.Dlna/Service/BaseService.cs @@ -12,12 +12,12 @@ namespace Emby.Dlna.Service protected IHttpClient HttpClient; protected ILogger Logger; - protected BaseService(ILogger logger, IHttpClient httpClient) + protected BaseService(ILogger logger, IHttpClient httpClient) { Logger = logger; HttpClient = httpClient; - EventManager = new EventManager(Logger, HttpClient); + EventManager = new EventManager(logger, HttpClient); } public EventSubscriptionResponse CancelEventSubscription(string subscriptionId) diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs index 47778acac..c9fc8d55a 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/Emby.Drawing/ImageProcessor.cs @@ -28,7 +28,7 @@ namespace Emby.Drawing private static readonly HashSet _transparentImageTypes = new HashSet(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" }; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IFileSystem _fileSystem; private readonly IServerApplicationPaths _appPaths; private readonly IImageEncoder _imageEncoder; diff --git a/Emby.Notifications/NotificationEntryPoint.cs b/Emby.Notifications/NotificationEntryPoint.cs index 869b7407e..b923fd26c 100644 --- a/Emby.Notifications/NotificationEntryPoint.cs +++ b/Emby.Notifications/NotificationEntryPoint.cs @@ -25,7 +25,7 @@ namespace Emby.Notifications /// public class NotificationEntryPoint : IServerEntryPoint { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IActivityManager _activityManager; private readonly ILocalizationManager _localization; private readonly INotificationManager _notificationManager; diff --git a/Emby.Notifications/NotificationManager.cs b/Emby.Notifications/NotificationManager.cs index 639a5e1aa..2792a8f0b 100644 --- a/Emby.Notifications/NotificationManager.cs +++ b/Emby.Notifications/NotificationManager.cs @@ -21,7 +21,7 @@ namespace Emby.Notifications /// public class NotificationManager : INotificationManager { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IUserManager _userManager; private readonly IServerConfigurationManager _config; diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs index 987cb7fb2..58baa1e50 100644 --- a/Emby.Photos/PhotoProvider.cs +++ b/Emby.Photos/PhotoProvider.cs @@ -22,7 +22,7 @@ namespace Emby.Photos /// public class PhotoProvider : ICustomMetadataProvider, IForcedProvider, IHasItemChangeMonitor { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IImageProcessor _imageProcessor; // These are causing taglib to hang diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index 080cfbbd1..d4a8268b9 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.AppBase CommonApplicationPaths = applicationPaths; XmlSerializer = xmlSerializer; _fileSystem = fileSystem; - Logger = loggerFactory.CreateLogger(GetType().Name); + Logger = loggerFactory.CreateLogger(); UpdateCachePath(); } @@ -83,7 +83,7 @@ namespace Emby.Server.Implementations.AppBase /// Gets the logger. /// /// The logger. - protected ILogger Logger { get; private set; } + protected ILogger Logger { get; private set; } /// /// Gets the XML serializer. diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index be4e05a64..184c6f399 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -173,7 +173,7 @@ namespace Emby.Server.Implementations /// /// Gets the logger. /// - protected ILogger Logger { get; } + protected ILogger Logger { get; } private IPlugin[] _plugins; diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs index 7f7c6a0be..7a0294e07 100644 --- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs +++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs @@ -41,7 +41,7 @@ namespace Emby.Server.Implementations.Browser } catch (Exception ex) { - var logger = appHost.Resolve(); + var logger = appHost.Resolve>(); logger?.LogError(ex, "Failed to open browser window with URL {URL}", relativeUrl); } } diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 04fe0bacb..f9eea66b4 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Channels private readonly IUserDataManager _userDataManager; private readonly IDtoService _dtoService; private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; private readonly IJsonSerializer _jsonSerializer; diff --git a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs index 54b621e25..e5dde48d8 100644 --- a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs +++ b/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs @@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.Channels public class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask { private readonly IChannelManager _channelManager; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index 7c518d483..cba3975cf 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -29,7 +29,7 @@ namespace Emby.Server.Implementations.Collections private readonly ILibraryManager _libraryManager; private readonly IFileSystem _fileSystem; private readonly ILibraryMonitor _iLibraryMonitor; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IProviderManager _providerManager; private readonly ILocalizationManager _localizationManager; private readonly IApplicationPaths _appPaths; @@ -56,7 +56,7 @@ namespace Emby.Server.Implementations.Collections _libraryManager = libraryManager; _fileSystem = fileSystem; _iLibraryMonitor = iLibraryMonitor; - _logger = loggerFactory.CreateLogger(nameof(CollectionManager)); + _logger = loggerFactory.CreateLogger(); _providerManager = providerManager; _localizationManager = localizationManager; _appPaths = appPaths; @@ -370,7 +370,7 @@ namespace Emby.Server.Implementations.Collections { private readonly CollectionManager _collectionManager; private readonly IServerConfigurationManager _config; - private readonly ILogger _logger; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 0654132f4..f816fd54f 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.Data /// Initializes a new instance of the class. /// /// The logger. - protected BaseSqliteRepository(ILogger logger) + protected BaseSqliteRepository(ILogger logger) { Logger = logger; } @@ -32,7 +32,7 @@ namespace Emby.Server.Implementations.Data /// Gets the logger. /// /// The logger. - protected ILogger Logger { get; } + protected ILogger Logger { get; } /// /// Gets the default connection flags. diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index 37c678a5d..6c9bcff0f 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -12,7 +12,7 @@ namespace Emby.Server.Implementations.Data public class CleanDatabaseScheduledTask : ILibraryPostScanTask { private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; + private readonly ILogger _logger; public CleanDatabaseScheduledTask(ILibraryManager libraryManager, ILogger logger) { diff --git a/Emby.Server.Implementations/Devices/DeviceId.cs b/Emby.Server.Implementations/Devices/DeviceId.cs index f0d43e665..fa6ac95fd 100644 --- a/Emby.Server.Implementations/Devices/DeviceId.cs +++ b/Emby.Server.Implementations/Devices/DeviceId.cs @@ -12,7 +12,7 @@ namespace Emby.Server.Implementations.Devices public class DeviceId { private readonly IApplicationPaths _appPaths; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly object _syncLock = new object(); @@ -90,7 +90,7 @@ namespace Emby.Server.Implementations.Devices public DeviceId(IApplicationPaths appPaths, ILoggerFactory loggerFactory) { _appPaths = appPaths; - _logger = loggerFactory.CreateLogger("SystemId"); + _logger = loggerFactory.CreateLogger(); } public string Value => _id ?? (_id = GetDeviceId()); diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 38c4f940d..f9841082c 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -29,7 +29,7 @@ namespace Emby.Server.Implementations.Dto { public class DtoService : IDtoService { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly IUserDataManager _userDataRepository; private readonly IItemRepository _itemRepo; diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index 878cee23c..9fce49425 100644 --- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -23,7 +23,7 @@ namespace Emby.Server.Implementations.EntryPoints public class ExternalPortForwarding : IServerEntryPoint { private readonly IServerApplicationHost _appHost; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IServerConfigurationManager _config; private readonly IDeviceDiscovery _deviceDiscovery; diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 9bc2b62ec..b9992ae73 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.EntryPoints private readonly ISessionManager _sessionManager; private readonly IUserManager _userManager; - private readonly ILogger _logger; + private readonly ILogger _logger; /// /// The library changed sync lock. diff --git a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs index 997571a91..b64439802 100644 --- a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs @@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.EntryPoints private readonly ILiveTvManager _liveTvManager; private readonly ISessionManager _sessionManager; private readonly IUserManager _userManager; - private readonly ILogger _logger; + private readonly ILogger _logger; public RecordingNotifier( ISessionManager sessionManager, diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 5bc1a81aa..ecdce89ce 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.EntryPoints /// /// The logger. /// - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IServerApplicationHost _appHost; private readonly IConfiguration _config; diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs index d66bb7638..87977494a 100644 --- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs +++ b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs @@ -22,7 +22,7 @@ namespace Emby.Server.Implementations.HttpClientManager /// public class HttpClientManager : IHttpClient { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; private readonly IFileSystem _fileSystem; private readonly IApplicationHost _appHost; diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 7de4f168c..17994d199 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -41,7 +41,7 @@ namespace Emby.Server.Implementations.HttpServer /// public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath"; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; private readonly IServerConfigurationManager _config; private readonly INetworkManager _networkManager; @@ -397,7 +397,7 @@ namespace Emby.Server.Implementations.HttpServer var response = context.Response; var localPath = context.Request.Path.ToString(); - var req = new WebSocketSharpRequest(request, response, request.Path, _logger); + var req = new WebSocketSharpRequest(request, response, request.Path); return RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted); } diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index 9d5969583..cc4797790 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -37,7 +37,7 @@ namespace Emby.Server.Implementations.HttpServer /// /// The logger. /// - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IFileSystem _fileSystem; private readonly IJsonSerializer _jsonSerializer; private readonly IStreamHelper _streamHelper; @@ -50,7 +50,7 @@ namespace Emby.Server.Implementations.HttpServer _fileSystem = fileSystem; _jsonSerializer = jsonSerializer; _streamHelper = streamHelper; - _logger = loggerfactory.CreateLogger("HttpResultFactory"); + _logger = loggerfactory.CreateLogger(); } /// diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index 256b24924..9bb29586e 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes) { - var req = new WebSocketSharpRequest(request, null, request.Path, _logger); + var req = new WebSocketSharpRequest(request, null, request.Path); var user = ValidateUser(req, authAttributes); return user; } diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index 0680c5ffe..c64d57339 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.HttpServer /// /// The logger. /// - private readonly ILogger _logger; + private readonly ILogger _logger; /// /// The json serializer options. diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index eb5e190aa..49bca7dac 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -18,7 +18,7 @@ namespace Emby.Server.Implementations.IO { public class LibraryMonitor : ILibraryMonitor { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly IServerConfigurationManager _configurationManager; private readonly IFileSystem _fileSystem; diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 7461ec4f1..2bcfc82b6 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -20,7 +20,7 @@ namespace Emby.Server.Implementations.IO /// public class ManagedFileSystem : IFileSystem { - protected ILogger Logger; + protected ILogger Logger; private readonly List _shortcutHandlers = new List(); private readonly string _tempPath; diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 677030b82..8c6be8e04 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -56,7 +56,7 @@ namespace Emby.Server.Implementations.Library /// public class LibraryManager : ILibraryManager { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly ITaskManager _taskManager; private readonly IUserManager _userManager; private readonly IUserDataManager _userDataRepository; diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index a5e5981b8..00826c7cb 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.Library private readonly ILibraryManager _libraryManager; private readonly IJsonSerializer _jsonSerializer; private readonly IFileSystem _fileSystem; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IUserDataManager _userDataManager; private readonly IMediaEncoder _mediaEncoder; private readonly ILocalizationManager _localizationManager; diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index 6c9ba7c27..ebfe95d0a 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -109,7 +109,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio IEnumerable list, bool allowSubfolders, IDirectoryService directoryService, - ILogger logger, + ILogger logger, IFileSystem fileSystem, ILibraryManager libraryManager) { diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index 18145b7f1..7f8800a64 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV private readonly IServerConfigurationManager _config; private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; - private readonly ILogger _logger; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index dd6bd8ee8..4180dad36 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -20,7 +20,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV public class SeriesResolver : FolderResolver { private readonly IFileSystem _fileSystem; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; /// @@ -119,7 +119,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV IEnumerable fileSystemChildren, IDirectoryService directoryService, IFileSystem fileSystem, - ILogger logger, + ILogger logger, ILibraryManager libraryManager, bool isTvContentType) { diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index 59a77607d..bde77ee8e 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.Library { public class SearchEngine : ISearchEngine { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index a9772a078..d8694b07d 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.Library private readonly ConcurrentDictionary _userData = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IServerConfigurationManager _config; private readonly IUserManager _userManager; private readonly IUserDataRepository _repository; diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 140155d0e..531e00666 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -46,7 +46,7 @@ namespace Emby.Server.Implementations.Library private readonly object _policySyncLock = new object(); private readonly object _configSyncLock = new object(); - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IUserRepository _userRepository; private readonly IXmlSerializer _xmlSerializer; private readonly IJsonSerializer _jsonSerializer; diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs index 2af8ff5cb..d51f9aaa7 100644 --- a/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs @@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Validators /// The _library manager. /// private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IItemRepository _itemRepo; /// @@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.Library.Validators /// The item repository. public ArtistsPostScanTask( ILibraryManager libraryManager, - ILogger logger, + ILogger logger, IItemRepository itemRepo) { _libraryManager = libraryManager; diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs index 1497f4a3a..8a6bd5e78 100644 --- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs @@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.Library.Validators /// /// The logger. /// - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IItemRepository _itemRepo; /// @@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.Library.Validators /// The library manager. /// The logger. /// The item repository. - public ArtistsValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + public ArtistsValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) { _libraryManager = libraryManager; _logger = logger; diff --git a/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs index 251785dfd..d21d2887b 100644 --- a/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs @@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Validators /// The _library manager. /// private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IItemRepository _itemRepo; /// @@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.Library.Validators /// The item repository. public GenresPostScanTask( ILibraryManager libraryManager, - ILogger logger, + ILogger logger, IItemRepository itemRepo) { _libraryManager = libraryManager; diff --git a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs index b0cd5f87a..e59c62e23 100644 --- a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs @@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.Library.Validators /// /// The logger. /// - private readonly ILogger _logger; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. @@ -29,7 +29,7 @@ namespace Emby.Server.Implementations.Library.Validators /// The library manager. /// The logger. /// The item repository. - public GenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + public GenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) { _libraryManager = libraryManager; _logger = logger; diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs index 9d8690116..be119866b 100644 --- a/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs @@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Validators /// The library manager. /// private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IItemRepository _itemRepo; /// @@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.Library.Validators /// The item repository. public MusicGenresPostScanTask( ILibraryManager libraryManager, - ILogger logger, + ILogger logger, IItemRepository itemRepo) { _libraryManager = libraryManager; diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs index 5ee4ca72e..1ecf4c87c 100644 --- a/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs @@ -20,7 +20,7 @@ namespace Emby.Server.Implementations.Library.Validators /// /// The logger. /// - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IItemRepository _itemRepo; /// @@ -29,7 +29,7 @@ namespace Emby.Server.Implementations.Library.Validators /// The library manager. /// The logger. /// The item repository. - public MusicGenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + public MusicGenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) { _libraryManager = libraryManager; _logger = logger; diff --git a/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs index 2f8f906b9..c682b156b 100644 --- a/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs @@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.Library.Validators /// private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IItemRepository _itemRepo; /// @@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Library.Validators /// The item repository. public StudiosPostScanTask( ILibraryManager libraryManager, - ILogger logger, + ILogger logger, IItemRepository itemRepo) { _libraryManager = libraryManager; diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs index 15e7a0dbb..7a6cd11df 100644 --- a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs @@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.Library.Validators /// /// The logger. /// - private readonly ILogger _logger; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. @@ -32,7 +32,7 @@ namespace Emby.Server.Implementations.Library.Validators /// The library manager. /// The logger. /// The item repository. - public StudiosValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + public StudiosValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) { _libraryManager = libraryManager; _logger = logger; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 5a5dc3329..f9b71939b 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -46,7 +46,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private const int TunerDiscoveryDurationMs = 3000; private readonly IServerApplicationHost _appHost; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IHttpClient _httpClient; private readonly IServerConfigurationManager _config; private readonly IJsonSerializer _jsonSerializer; diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 89b81fd96..02f18060d 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings { public class SchedulesDirect : IListingsProvider { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClient _httpClient; private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1); diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs index 07f8539c5..077b5c7e5 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings { private readonly IServerConfigurationManager _config; private readonly IHttpClient _httpClient; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IFileSystem _fileSystem; private readonly IZipClient _zipClient; diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs index a59c1090e..49ad73af3 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.LiveTv private const string ServiceName = "Emby"; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IImageProcessor _imageProcessor; private readonly IDtoService _dtoService; private readonly IApplicationHost _appHost; diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index dbd0e6f2e..a88e0f31f 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.LiveTv private const string EtagKey = "ProgramEtag"; private readonly IServerConfigurationManager _config; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IItemRepository _itemRepo; private readonly IUserManager _userManager; private readonly IDtoService _dtoService; diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs index 7f63991d0..f3fc41352 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs @@ -23,7 +23,7 @@ namespace Emby.Server.Implementations.LiveTv private const string StreamIdDelimeterString = "_"; private readonly ILiveTvManager _liveTvManager; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerApplicationHost _appHost; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs index 80ee1ee33..ba3594efd 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs @@ -22,14 +22,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts public abstract class BaseTunerHost { protected readonly IServerConfigurationManager Config; - protected readonly ILogger Logger; + protected readonly ILogger Logger; protected IJsonSerializer JsonSerializer; protected readonly IFileSystem FileSystem; private readonly ConcurrentDictionary _channelCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - protected BaseTunerHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem) + protected BaseTunerHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem) { Config = config; Logger = logger; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs index 4e4f1d7f6..c3336ca92 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Emby.Server.Implementations.Library; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index e2a634e1a..62a23118f 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Localization private readonly IServerConfigurationManager _configurationManager; private readonly IJsonSerializer _jsonSerializer; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly Dictionary> _allParentalRatings = new Dictionary>(StringComparer.OrdinalIgnoreCase); diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index 7b7575707..438bbe24a 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -23,7 +23,7 @@ namespace Emby.Server.Implementations.MediaEncoder { private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly IFileSystem _fileSystem; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IMediaEncoder _encoder; private readonly IChapterManager _chapterManager; private readonly ILibraryManager _libraryManager; diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs index d1a28e7a1..45864bb42 100644 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ b/Emby.Server.Implementations/Networking/NetworkManager.cs @@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Networking { public class NetworkManager : INetworkManager { - private readonly ILogger _logger; + private readonly ILogger _logger; private IPAddress[] _localIpAddresses; private readonly object _localIpAddressSyncLock = new object(); diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index c51eb0586..8d022d6c4 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -29,7 +29,7 @@ namespace Emby.Server.Implementations.Playlists private readonly ILibraryManager _libraryManager; private readonly IFileSystem _fileSystem; private readonly ILibraryMonitor _iLibraryMonitor; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IUserManager _userManager; private readonly IProviderManager _providerManager; private readonly IConfiguration _appConfig; diff --git a/Emby.Server.Implementations/ResourceFileManager.cs b/Emby.Server.Implementations/ResourceFileManager.cs index d192be921..22fc62293 100644 --- a/Emby.Server.Implementations/ResourceFileManager.cs +++ b/Emby.Server.Implementations/ResourceFileManager.cs @@ -12,7 +12,7 @@ namespace Emby.Server.Implementations public class ResourceFileManager : IResourceFileManager { private readonly IFileSystem _fileSystem; - private readonly ILogger _logger; + private readonly ILogger _logger; public ResourceFileManager(ILogger logger, IFileSystem fileSystem) { diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs index efefa5506..94220ac5d 100644 --- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs +++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs @@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.ScheduledTasks private readonly IJsonSerializer _jsonSerializer; private readonly IApplicationPaths _applicationPaths; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IFileSystem _fileSystem; /// diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index fae049914..9028222dd 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// /// The _logger. /// - private readonly ILogger _logger; + private readonly ILogger _logger; /// /// The _library manager. @@ -54,7 +54,7 @@ namespace Emby.Server.Implementations.ScheduledTasks IFileSystem fileSystem, ILocalizationManager localization) { - _logger = loggerFactory.CreateLogger(GetType().Name); + _logger = loggerFactory.CreateLogger(); _libraryManager = libraryManager; _itemRepo = itemRepo; _appPaths = appPaths; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs index a6c13eaef..966b549b2 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs @@ -23,7 +23,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// The application paths. private IApplicationPaths ApplicationPaths { get; set; } - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IFileSystem _fileSystem; private readonly ILocalizationManager _localization; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs index 0d36b82c0..53cf9a0a5 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs @@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// public class DeleteTranscodeFileTask : IScheduledTask, IConfigurableScheduledTask { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IConfigurationManager _configurationManager; private readonly IFileSystem _fileSystem; private readonly ILocalizationManager _localization; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs index acab3aeea..7388086fb 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs @@ -22,7 +22,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// /// The _logger. /// - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IInstallationManager _installationManager; private readonly ILocalizationManager _localization; diff --git a/Emby.Server.Implementations/Services/ServiceController.cs b/Emby.Server.Implementations/Services/ServiceController.cs index ad6015c1c..e688278b5 100644 --- a/Emby.Server.Implementations/Services/ServiceController.cs +++ b/Emby.Server.Implementations/Services/ServiceController.cs @@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Services public class ServiceController { - private readonly ILogger _logger; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 5c480e842..506e6739f 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.Session /// /// The logger. /// - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index e7b4b0ec3..ef32c692c 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -41,7 +41,7 @@ namespace Emby.Server.Implementations.Session /// /// The _logger /// - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; private readonly IHttpServer _httpServer; diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs index a0274acd2..94604ca1e 100644 --- a/Emby.Server.Implementations/Session/WebSocketController.cs +++ b/Emby.Server.Implementations/Session/WebSocketController.cs @@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.Session { public sealed class WebSocketController : ISessionController, IDisposable { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly ISessionManager _sessionManager; private readonly SessionInfo _session; diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index aa76901a4..ae1a8d0b7 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.SocketSharp private Dictionary _items; private string _responseContentType; - public WebSocketSharpRequest(HttpRequest httpRequest, HttpResponse httpResponse, string operationName, ILogger logger) + public WebSocketSharpRequest(HttpRequest httpRequest, HttpResponse httpResponse, string operationName) { this.OperationName = operationName; this.Request = httpRequest; diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs index 129262e53..1bfc9a9a5 100644 --- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs @@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.SyncPlay /// /// The logger. /// - private readonly ILogger _logger; + private readonly ILogger _logger; /// /// The user manager. diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 178f32c31..80326fddf 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -39,7 +39,7 @@ namespace Emby.Server.Implementations.Updates /// /// The logger. /// - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; private readonly IHttpClient _httpClient; private readonly IJsonSerializer _jsonSerializer; diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index ccee5c5b9..ba9a5809f 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -21,7 +21,7 @@ namespace Jellyfin.Drawing.Skia private static readonly HashSet _transparentImageTypes = new HashSet(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" }; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; /// diff --git a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs index b2e957d5b..c18aa1629 100644 --- a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs +++ b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs @@ -10,7 +10,7 @@ namespace Jellyfin.Server.Migrations.Routines /// internal class DisableTranscodingThrottling : IMigrationRoutine { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IConfigurationManager _configManager; public DisableTranscodingThrottling(ILogger logger, IConfigurationManager configManager) diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs index e95536388..2ebef0241 100644 --- a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs +++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs @@ -14,7 +14,7 @@ namespace Jellyfin.Server.Migrations.Routines internal class RemoveDuplicateExtras : IMigrationRoutine { private const string DbFilename = "library.db"; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IServerApplicationPaths _paths; public RemoveDuplicateExtras(ILogger logger, IServerApplicationPaths paths) diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index c7485a2e9..9e651fb19 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -31,7 +31,7 @@ namespace MediaBrowser.Api /// /// The logger. /// - private ILogger _logger; + private ILogger _logger; /// /// The configuration manager. diff --git a/MediaBrowser.Api/Music/AlbumsService.cs b/MediaBrowser.Api/Music/AlbumsService.cs index 58c95d053..f257d1014 100644 --- a/MediaBrowser.Api/Music/AlbumsService.cs +++ b/MediaBrowser.Api/Music/AlbumsService.cs @@ -67,12 +67,13 @@ namespace MediaBrowser.Api.Music { var dtoOptions = GetDtoOptions(_authContext, request); - var result = SimilarItemsHelper.GetSimilarItemsResult(dtoOptions, _userManager, + var result = SimilarItemsHelper.GetSimilarItemsResult( + dtoOptions, + _userManager, _itemRepo, _libraryManager, _userDataRepository, _dtoService, - Logger, request, new[] { typeof(MusicArtist) }, SimilarItemsHelper.GetSimiliarityScore); @@ -88,12 +89,13 @@ namespace MediaBrowser.Api.Music { var dtoOptions = GetDtoOptions(_authContext, request); - var result = SimilarItemsHelper.GetSimilarItemsResult(dtoOptions, _userManager, + var result = SimilarItemsHelper.GetSimilarItemsResult( + dtoOptions, + _userManager, _itemRepo, _libraryManager, _userDataRepository, _dtoService, - Logger, request, new[] { typeof(MusicAlbum) }, GetAlbumSimilarityScore); diff --git a/MediaBrowser.Api/SimilarItemsHelper.cs b/MediaBrowser.Api/SimilarItemsHelper.cs index 44bb24ef2..dcd22280a 100644 --- a/MediaBrowser.Api/SimilarItemsHelper.cs +++ b/MediaBrowser.Api/SimilarItemsHelper.cs @@ -69,7 +69,7 @@ namespace MediaBrowser.Api /// public static class SimilarItemsHelper { - internal static QueryResult GetSimilarItemsResult(DtoOptions dtoOptions, IUserManager userManager, IItemRepository itemRepository, ILibraryManager libraryManager, IUserDataManager userDataRepository, IDtoService dtoService, ILogger logger, BaseGetSimilarItemsFromItem request, Type[] includeTypes, Func, List, BaseItem, int> getSimilarityScore) + internal static QueryResult GetSimilarItemsResult(DtoOptions dtoOptions, IUserManager userManager, IItemRepository itemRepository, ILibraryManager libraryManager, IUserDataManager userDataRepository, IDtoService dtoService, BaseGetSimilarItemsFromItem request, Type[] includeTypes, Func, List, BaseItem, int> getSimilarityScore) { var user = !request.UserId.Equals(Guid.Empty) ? userManager.GetUserById(request.UserId) : null; diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index f4b71d8bf..2c789e386 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -557,7 +557,8 @@ namespace MediaBrowser.Controller.Entities /// /// The logger /// - public static ILogger Logger { get; set; } + public static ILoggerFactory LoggerFactory { get; set; } + public static ILogger Logger { get; set; } public static ILibraryManager LibraryManager { get; set; } public static IServerConfigurationManager ConfigurationManager { get; set; } public static IProviderManager ProviderManager { get; set; } diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index 4ce9ec6f8..86cff632c 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -5,6 +5,7 @@ using System.Text.Json.Serialization; using System.Threading.Tasks; using MediaBrowser.Controller.TV; using MediaBrowser.Model.Querying; +using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities { @@ -66,7 +67,7 @@ namespace MediaBrowser.Controller.Entities parent = LibraryManager.GetItemById(ParentId) as Folder ?? parent; } - return new UserViewBuilder(UserViewManager, LibraryManager, Logger, UserDataManager, TVSeriesManager, ConfigurationManager) + return new UserViewBuilder(UserViewManager, LibraryManager, LoggerFactory.CreateLogger(), UserDataManager, TVSeriesManager, ConfigurationManager) .GetUserItems(parent, this, CollectionType, query); } diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 435a1e8da..3d91a0d0e 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -17,7 +17,7 @@ namespace MediaBrowser.Controller.Entities { private readonly IUserViewManager _userViewManager; private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IUserDataManager _userDataManager; private readonly ITVSeriesManager _tvSeriesManager; private readonly IServerConfigurationManager _config; @@ -25,7 +25,7 @@ namespace MediaBrowser.Controller.Entities public UserViewBuilder( IUserViewManager userViewManager, ILibraryManager libraryManager, - ILogger logger, + ILogger logger, IUserDataManager userDataManager, ITVSeriesManager tvSeriesManager, IServerConfigurationManager config) diff --git a/MediaBrowser.Controller/IO/FileData.cs b/MediaBrowser.Controller/IO/FileData.cs index 4bbb60283..666a3f76b 100644 --- a/MediaBrowser.Controller/IO/FileData.cs +++ b/MediaBrowser.Controller/IO/FileData.cs @@ -35,7 +35,8 @@ namespace MediaBrowser.Controller.IO /// if set to true [resolve shortcuts]. /// Dictionary{System.StringFileSystemInfo}. /// path - public static FileSystemMetadata[] GetFilteredFileSystemEntries(IDirectoryService directoryService, + public static FileSystemMetadata[] GetFilteredFileSystemEntries( + IDirectoryService directoryService, string path, IFileSystem fileSystem, IServerApplicationHost appHost, diff --git a/MediaBrowser.Controller/Library/Profiler.cs b/MediaBrowser.Controller/Library/Profiler.cs index 46a97d181..0febef3d3 100644 --- a/MediaBrowser.Controller/Library/Profiler.cs +++ b/MediaBrowser.Controller/Library/Profiler.cs @@ -21,7 +21,7 @@ namespace MediaBrowser.Controller.Library /// /// The _logger /// - private readonly ILogger _logger; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index 5be656bdb..7dca793c6 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -40,9 +40,9 @@ namespace MediaBrowser.Controller.Net /// /// The logger /// - protected ILogger Logger; + protected ILogger> Logger; - protected BasePeriodicWebSocketListener(ILogger logger) + protected BasePeriodicWebSocketListener(ILogger> logger) { if (logger == null) { diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index 2ba7c9fec..ecc910872 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; diff --git a/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs b/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs index 795933ce9..5137e4c68 100644 --- a/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.LocalMetadata.Images { private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; - private readonly ILogger _logger; + private readonly ILogger _logger; public InternalMetadataFolderImageProvider( IServerConfigurationManager config, diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index d4b98182f..9621cfa4e 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -23,7 +23,7 @@ namespace MediaBrowser.LocalMetadata.Parsers /// /// The logger /// - protected ILogger Logger { get; private set; } + protected ILogger> Logger { get; private set; } protected IProviderManager ProviderManager { get; private set; } private Dictionary _validProviderIds; @@ -32,7 +32,7 @@ namespace MediaBrowser.LocalMetadata.Parsers /// Initializes a new instance of the class. /// /// The logger. - public BaseItemXmlParser(ILogger logger, IProviderManager providerManager) + public BaseItemXmlParser(ILogger> logger, IProviderManager providerManager) { Logger = logger; ProviderManager = providerManager; diff --git a/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs index 127334625..ca11a079d 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs @@ -85,7 +85,7 @@ namespace MediaBrowser.LocalMetadata.Parsers item.Item.LinkedChildren = list.ToArray(); } - public BoxSetXmlParser(ILogger logger, IProviderManager providerManager) + public BoxSetXmlParser(ILogger logger, IProviderManager providerManager) : base(logger, providerManager) { } diff --git a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs index 5608a0be9..54710cd82 100644 --- a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs @@ -93,7 +93,7 @@ namespace MediaBrowser.LocalMetadata.Parsers item.LinkedChildren = list.ToArray(); } - public PlaylistXmlParser(ILogger logger, IProviderManager providerManager) + public PlaylistXmlParser(ILogger logger, IProviderManager providerManager) : base(logger, providerManager) { } diff --git a/MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs b/MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs index b2e3bc9e2..2d115a591 100644 --- a/MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs +++ b/MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs @@ -13,10 +13,10 @@ namespace MediaBrowser.LocalMetadata.Providers /// public class BoxSetXmlProvider : BaseXmlProvider { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IProviderManager _providerManager; - public BoxSetXmlProvider(IFileSystem fileSystem, ILogger logger, IProviderManager providerManager) + public BoxSetXmlProvider(IFileSystem fileSystem, ILogger logger, IProviderManager providerManager) : base(fileSystem) { _logger = logger; diff --git a/MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs b/MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs index df8107bad..d4e2bc8e5 100644 --- a/MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs +++ b/MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs @@ -10,12 +10,12 @@ namespace MediaBrowser.LocalMetadata.Providers { public class PlaylistXmlProvider : BaseXmlProvider { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IProviderManager _providerManager; public PlaylistXmlProvider( IFileSystem fileSystem, - ILogger logger, + ILogger logger, IProviderManager providerManager) : base(fileSystem) { diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs index ba1d850e3..071902393 100644 --- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs @@ -21,7 +21,7 @@ namespace MediaBrowser.LocalMetadata.Savers { private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - public BaseXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger) + public BaseXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger) { FileSystem = fileSystem; ConfigurationManager = configurationManager; @@ -36,7 +36,7 @@ namespace MediaBrowser.LocalMetadata.Savers protected ILibraryManager LibraryManager { get; private set; } protected IUserManager UserManager { get; private set; } protected IUserDataManager UserDataManager { get; private set; } - protected ILogger Logger { get; private set; } + protected ILogger Logger { get; private set; } public string Name => XmlProviderUtils.Name; diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index 3f177a9fa..a82f2108f 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -21,7 +21,7 @@ namespace MediaBrowser.MediaEncoding.Attachments { public class AttachmentExtractor : IAttachmentExtractor, IDisposable { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; private readonly IFileSystem _fileSystem; private readonly IMediaEncoder _mediaEncoder; diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 1377502dd..26bd202ba 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -34,7 +34,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// internal const int DefaultImageExtractionTimeout = 5000; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IServerConfigurationManager _configurationManager; private readonly IFileSystem _fileSystem; private readonly ILocalizationManager _localization; diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index ba171295e..f08af6045 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -25,7 +25,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles public class SubtitleEncoder : ISubtitleEncoder { private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; private readonly IFileSystem _fileSystem; private readonly IMediaEncoder _mediaEncoder; diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs index 3ab621ba4..f21ac2d65 100644 --- a/MediaBrowser.Providers/Manager/ImageSaver.cs +++ b/MediaBrowser.Providers/Manager/ImageSaver.cs @@ -15,6 +15,7 @@ using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; +using MediaBrowser.Providers.Tmdb.Models.General; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Manager diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index c49aa407a..2997ba064 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -20,12 +20,12 @@ namespace MediaBrowser.Providers.Manager where TIdType : ItemLookupInfo, new() { protected readonly IServerConfigurationManager ServerConfigurationManager; - protected readonly ILogger Logger; + protected readonly ILogger> Logger; protected readonly IProviderManager ProviderManager; protected readonly IFileSystem FileSystem; protected readonly ILibraryManager LibraryManager; - protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) + protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) { ServerConfigurationManager = serverConfigurationManager; Logger = logger; diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 400ec8d29..13a4436e1 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -36,7 +36,7 @@ namespace MediaBrowser.Providers.Manager /// public class ProviderManager : IProviderManager, IDisposable { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IHttpClient _httpClient; private readonly ILibraryMonitor _libraryMonitor; private readonly IFileSystem _fileSystem; diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs index 6982568eb..81103575d 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs @@ -37,7 +37,7 @@ namespace MediaBrowser.Providers.MediaInfo IPreRefreshProvider, IHasItemChangeMonitor { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IIsoManager _isoManager; private readonly IMediaEncoder _mediaEncoder; private readonly IItemRepository _itemRepo; diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs index 77c0e9b4e..f35e82bb5 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs @@ -25,7 +25,8 @@ namespace MediaBrowser.Providers.MediaInfo _subtitleManager = subtitleManager; } - public async Task> DownloadSubtitles(Video video, + public async Task> DownloadSubtitles( + Video video, List mediaStreams, bool skipIfEmbeddedSubtitlesPresent, bool skipIfAudioTrackMatches, diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs index 2615f2dbb..fb87030ff 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs @@ -24,7 +24,7 @@ namespace MediaBrowser.Providers.MediaInfo private readonly IServerConfigurationManager _config; private readonly ISubtitleManager _subtitleManager; private readonly IMediaSourceManager _mediaSourceManager; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IJsonSerializer _json; private readonly ILocalizationManager _localization; diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs index f40570040..08e503a71 100644 --- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs @@ -17,7 +17,7 @@ namespace MediaBrowser.Providers.MediaInfo public class VideoImageProvider : IDynamicImageProvider, IHasOrder { private readonly IMediaEncoder _mediaEncoder; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IFileSystem _fileSystem; public VideoImageProvider(IMediaEncoder mediaEncoder, ILogger logger, IFileSystem fileSystem) diff --git a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs index ae837c591..9fc86b213 100644 --- a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs +++ b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs @@ -20,7 +20,7 @@ namespace MediaBrowser.Providers.Playlists IPreRefreshProvider, IHasItemChangeMonitor { - private ILogger _logger; + private readonly ILogger _logger; private IFileSystem _fileSystem; public PlaylistItemsProvider(IFileSystem fileSystem, ILogger logger) diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs index 31cdaf616..9d0d14969 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs @@ -42,7 +42,7 @@ namespace MediaBrowser.Providers.Music private readonly IHttpClient _httpClient; private readonly IApplicationHost _appHost; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly string _musicBrainzBaseUrl; diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs index 6118a9c53..7a6fdc5f4 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs @@ -17,7 +17,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb public class TvdbEpisodeImageProvider : IRemoteImageProvider { private readonly IHttpClient _httpClient; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly TvdbClientManager _tvdbClientManager; public TvdbEpisodeImageProvider(IHttpClient httpClient, ILogger logger, TvdbClientManager tvdbClientManager) diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs index 08c2a74d2..1f8b82ecd 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs @@ -21,7 +21,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb public class TvdbEpisodeProvider : IRemoteMetadataProvider, IHasOrder { private readonly IHttpClient _httpClient; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly TvdbClientManager _tvdbClientManager; public TvdbEpisodeProvider(IHttpClient httpClient, ILogger logger, TvdbClientManager tvdbClientManager) diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs index c1cdc90e9..961cb5970 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs @@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb public class TvdbPersonImageProvider : IRemoteImageProvider, IHasOrder { private readonly IHttpClient _httpClient; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly TvdbClientManager _tvdbClientManager; diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs index a5d183df7..cd22514b4 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs @@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb public class TvdbSeasonImageProvider : IRemoteImageProvider, IHasOrder { private readonly IHttpClient _httpClient; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly TvdbClientManager _tvdbClientManager; public TvdbSeasonImageProvider(IHttpClient httpClient, ILogger logger, TvdbClientManager tvdbClientManager) diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs index 1bad60756..3fd98b828 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs @@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb public class TvdbSeriesImageProvider : IRemoteImageProvider, IHasOrder { private readonly IHttpClient _httpClient; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly TvdbClientManager _tvdbClientManager; public TvdbSeriesImageProvider(IHttpClient httpClient, ILogger logger, TvdbClientManager tvdbClientManager) diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs index f6cd249f5..35152610a 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs @@ -23,7 +23,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb { internal static TvdbSeriesProvider Current { get; private set; } private readonly IHttpClient _httpClient; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localizationManager; private readonly TvdbClientManager _tvdbClientManager; diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index 127d29c04..c6ffc460c 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -25,7 +25,7 @@ namespace MediaBrowser.Providers.Subtitles { public class SubtitleManager : ISubtitleManager { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IFileSystem _fileSystem; private readonly ILibraryMonitor _monitor; private readonly IMediaSourceManager _mediaSourceManager; diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index 5e75a8125..a91d5d707 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -42,7 +42,8 @@ namespace MediaBrowser.Providers.TV await seasonProvider.Run(item, cancellationToken).ConfigureAwait(false); // TODO why does it not register this itself omg - var provider = new MissingEpisodeProvider(Logger, + var provider = new MissingEpisodeProvider( + Logger, ServerConfigurationManager, LibraryManager, _localization, diff --git a/MediaBrowser.Providers/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Tmdb/BoxSets/TmdbBoxSetProvider.cs index dd3783ffb..e17f5efdf 100644 --- a/MediaBrowser.Providers/Tmdb/BoxSets/TmdbBoxSetProvider.cs +++ b/MediaBrowser.Providers/Tmdb/BoxSets/TmdbBoxSetProvider.cs @@ -29,7 +29,7 @@ namespace MediaBrowser.Providers.Tmdb.BoxSets internal static TmdbBoxSetProvider Current; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IJsonSerializer _json; private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; diff --git a/MediaBrowser.Providers/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Tmdb/Movies/TmdbMovieProvider.cs index e2fd5b9e3..3787d4003 100644 --- a/MediaBrowser.Providers/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Tmdb/Movies/TmdbMovieProvider.cs @@ -36,7 +36,7 @@ namespace MediaBrowser.Providers.Tmdb.Movies private readonly IHttpClient _httpClient; private readonly IFileSystem _fileSystem; private readonly IServerConfigurationManager _configurationManager; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly IApplicationHost _appHost; diff --git a/MediaBrowser.Providers/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Tmdb/People/TmdbPersonProvider.cs index 588001169..1f09badbc 100644 --- a/MediaBrowser.Providers/Tmdb/People/TmdbPersonProvider.cs +++ b/MediaBrowser.Providers/Tmdb/People/TmdbPersonProvider.cs @@ -35,7 +35,7 @@ namespace MediaBrowser.Providers.Tmdb.People private readonly IFileSystem _fileSystem; private readonly IServerConfigurationManager _configurationManager; private readonly IHttpClient _httpClient; - private readonly ILogger _logger; + private readonly ILogger _logger; public TmdbPersonProvider( IFileSystem fileSystem, diff --git a/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeImageProvider.cs index 558c8149e..b0174a587 100644 --- a/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeImageProvider.cs @@ -24,8 +24,8 @@ namespace MediaBrowser.Providers.Tmdb.TV IRemoteImageProvider, IHasOrder { - public TmdbEpisodeImageProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory) - : base(httpClient, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory) + public TmdbEpisodeImageProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILogger logger) + : base(httpClient, configurationManager, jsonSerializer, fileSystem, localization, logger) { } public IEnumerable GetSupportedImages(BaseItem item) diff --git a/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeProvider.cs index a17f5d17a..330b9f42b 100644 --- a/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeProvider.cs @@ -25,8 +25,8 @@ namespace MediaBrowser.Providers.Tmdb.TV IRemoteMetadataProvider, IHasOrder { - public TmdbEpisodeProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory) - : base(httpClient, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory) + public TmdbEpisodeProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILogger logger) + : base(httpClient, configurationManager, jsonSerializer, fileSystem, localization, logger) { } public async Task> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeProviderBase.cs b/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeProviderBase.cs index e87fe9332..3937ebc95 100644 --- a/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeProviderBase.cs +++ b/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeProviderBase.cs @@ -24,14 +24,14 @@ namespace MediaBrowser.Providers.Tmdb.TV private readonly ILocalizationManager _localization; private readonly ILogger _logger; - protected TmdbEpisodeProviderBase(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory) + protected TmdbEpisodeProviderBase(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILogger logger) { _httpClient = httpClient; _configurationManager = configurationManager; _jsonSerializer = jsonSerializer; _fileSystem = fileSystem; _localization = localization; - _logger = loggerFactory.CreateLogger(GetType().Name); + _logger = logger; } protected ILogger Logger => _logger; diff --git a/MediaBrowser.Providers/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Tmdb/TV/TmdbSeasonProvider.cs index 5ad331971..832b64941 100644 --- a/MediaBrowser.Providers/Tmdb/TV/TmdbSeasonProvider.cs +++ b/MediaBrowser.Providers/Tmdb/TV/TmdbSeasonProvider.cs @@ -29,18 +29,18 @@ namespace MediaBrowser.Providers.Tmdb.TV private readonly IJsonSerializer _jsonSerializer; private readonly IFileSystem _fileSystem; private readonly ILocalizationManager _localization; - private readonly ILogger _logger; + private readonly ILogger _logger; internal static TmdbSeasonProvider Current { get; private set; } - public TmdbSeasonProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization, IJsonSerializer jsonSerializer, ILoggerFactory loggerFactory) + public TmdbSeasonProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization, IJsonSerializer jsonSerializer, ILogger logger) { _httpClient = httpClient; _configurationManager = configurationManager; _fileSystem = fileSystem; _localization = localization; _jsonSerializer = jsonSerializer; - _logger = loggerFactory.CreateLogger(GetType().Name); + _logger = logger; Current = this; } diff --git a/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs index 6e3c26c26..649c5474b 100644 --- a/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs @@ -31,7 +31,7 @@ namespace MediaBrowser.Providers.Tmdb.TV private readonly IJsonSerializer _jsonSerializer; private readonly IFileSystem _fileSystem; private readonly IServerConfigurationManager _configurationManager; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly ILocalizationManager _localization; private readonly IHttpClient _httpClient; private readonly ILibraryManager _libraryManager; diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 55fc463d0..63cbfd9e4 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -90,7 +90,7 @@ namespace MediaBrowser.WebDashboard.Api /// Gets or sets the logger. /// /// The logger. - private readonly ILogger _logger; + private readonly ILogger _logger; /// /// Gets or sets the HTTP result factory. diff --git a/MediaBrowser.XbmcMetadata/EntryPoint.cs b/MediaBrowser.XbmcMetadata/EntryPoint.cs index 571953b47..11b36285c 100644 --- a/MediaBrowser.XbmcMetadata/EntryPoint.cs +++ b/MediaBrowser.XbmcMetadata/EntryPoint.cs @@ -17,7 +17,7 @@ namespace MediaBrowser.XbmcMetadata public sealed class EntryPoint : IServerEntryPoint { private readonly IUserDataManager _userDataManager; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IProviderManager _providerManager; private readonly IConfigurationManager _config; diff --git a/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs index 4b1ee4c9c..433a936d9 100644 --- a/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs @@ -14,7 +14,7 @@ namespace MediaBrowser.XbmcMetadata.Providers /// public class AlbumNfoProvider : BaseNfoProvider { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IConfigurationManager _config; private readonly IProviderManager _providerManager; diff --git a/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs index 3bbfa6e83..d69cdc90a 100644 --- a/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs @@ -14,7 +14,7 @@ namespace MediaBrowser.XbmcMetadata.Providers /// public class ArtistNfoProvider : BaseNfoProvider { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IConfigurationManager _config; private readonly IProviderManager _providerManager; diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs index 84c99664a..2b1589d47 100644 --- a/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs @@ -15,12 +15,12 @@ namespace MediaBrowser.XbmcMetadata.Providers public abstract class BaseVideoNfoProvider : BaseNfoProvider where T : Video, new() { - private readonly ILogger _logger; + private readonly ILogger> _logger; private readonly IConfigurationManager _config; private readonly IProviderManager _providerManager; public BaseVideoNfoProvider( - ILogger logger, + ILogger> logger, IFileSystem fileSystem, IConfigurationManager config, IProviderManager providerManager) diff --git a/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs index b2dc2e38e..26983b1a6 100644 --- a/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs @@ -14,7 +14,7 @@ namespace MediaBrowser.XbmcMetadata.Providers /// public class EpisodeNfoProvider : BaseNfoProvider { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IConfigurationManager _config; private readonly IProviderManager _providerManager; diff --git a/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs index 63ddd6025..0603fd0d1 100644 --- a/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs @@ -14,7 +14,7 @@ namespace MediaBrowser.XbmcMetadata.Providers /// public class SeasonNfoProvider : BaseNfoProvider { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IConfigurationManager _config; private readonly IProviderManager _providerManager; diff --git a/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs index d65914542..7e059e0aa 100644 --- a/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs @@ -14,7 +14,7 @@ namespace MediaBrowser.XbmcMetadata.Providers /// public class SeriesNfoProvider : BaseNfoProvider { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IConfigurationManager _config; private readonly IProviderManager _providerManager; diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index 90e8b4b99..3602ec14c 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -105,7 +105,7 @@ namespace MediaBrowser.XbmcMetadata.Savers ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, - ILogger logger) + ILogger logger) { Logger = logger; UserDataManager = userDataManager; @@ -125,7 +125,7 @@ namespace MediaBrowser.XbmcMetadata.Savers protected IUserDataManager UserDataManager { get; } - protected ILogger Logger { get; } + protected ILogger Logger { get; } protected ItemUpdateType MinimumUpdateType { @@ -974,7 +974,7 @@ namespace MediaBrowser.XbmcMetadata.Savers => string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase) || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase); - private void AddCustomTags(string path, List xmlTagsUsed, XmlWriter writer, ILogger logger) + private void AddCustomTags(string path, List xmlTagsUsed, XmlWriter writer, ILogger logger) { var settings = new XmlReaderSettings() { -- cgit v1.2.3 From 7d9b5524031fe6b5c23b4282cb1f9ec850b114fe Mon Sep 17 00:00:00 2001 From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com> Date: Tue, 9 Jun 2020 13:28:40 -0500 Subject: Apply suggestions from code review Co-authored-by: Cody Robibero --- Emby.Server.Implementations/ApplicationHost.cs | 1 - .../QuickConnect/ConfigurationExtension.cs | 17 +++++------ .../QuickConnect/QuickConnectConfiguration.cs | 11 ++----- .../QuickConnect/QuickConnectManager.cs | 35 +++++++++++++++------- .../Session/SessionManager.cs | 2 +- 5 files changed, 36 insertions(+), 30 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 0a349bb33..51e63ecfc 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -646,7 +646,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); } - /// /// Create services registered with the service container that need to be initialized at application startup. /// diff --git a/Emby.Server.Implementations/QuickConnect/ConfigurationExtension.cs b/Emby.Server.Implementations/QuickConnect/ConfigurationExtension.cs index 349010039..596ded8ca 100644 --- a/Emby.Server.Implementations/QuickConnect/ConfigurationExtension.cs +++ b/Emby.Server.Implementations/QuickConnect/ConfigurationExtension.cs @@ -1,18 +1,17 @@ -using System.Collections.Generic; using MediaBrowser.Common.Configuration; namespace Emby.Server.Implementations.QuickConnect { /// - /// Configuration extension to support persistent quick connect configuration + /// Configuration extension to support persistent quick connect configuration. /// public static class ConfigurationExtension { /// - /// Return the current quick connect configuration + /// Return the current quick connect configuration. /// - /// Configuration manager - /// + /// Configuration manager. + /// Current quick connect configuration. public static QuickConnectConfiguration GetQuickConnectConfiguration(this IConfigurationManager manager) { return manager.GetConfiguration("quickconnect"); @@ -20,17 +19,17 @@ namespace Emby.Server.Implementations.QuickConnect } /// - /// Configuration factory for quick connect + /// Configuration factory for quick connect. /// public class QuickConnectConfigurationFactory : IConfigurationFactory { /// - /// Returns the current quick connect configuration + /// Returns the current quick connect configuration. /// - /// + /// Current quick connect configuration. public IEnumerable GetConfigurations() { - return new ConfigurationStore[] + return new[] { new ConfigurationStore { diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectConfiguration.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectConfiguration.cs index e1881f278..2302ddbc3 100644 --- a/Emby.Server.Implementations/QuickConnect/QuickConnectConfiguration.cs +++ b/Emby.Server.Implementations/QuickConnect/QuickConnectConfiguration.cs @@ -3,19 +3,12 @@ using MediaBrowser.Model.QuickConnect; namespace Emby.Server.Implementations.QuickConnect { /// - /// Persistent quick connect configuration + /// Persistent quick connect configuration. /// public class QuickConnectConfiguration { /// - /// Quick connect configuration object - /// - public QuickConnectConfiguration() - { - } - - /// - /// Persistent quick connect availability state + /// Gets or sets persistent quick connect availability state. /// public QuickConnectState State { get; set; } } diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs index 929e021a3..62b775fa6 100644 --- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs +++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs @@ -27,15 +27,11 @@ namespace Emby.Server.Implementations.QuickConnect private readonly RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider(); private Dictionary _currentRequests = new Dictionary(); - private IServerConfigurationManager _config; - private ILogger _logger; - private IUserManager _userManager; - private ILocalizationManager _localizationManager; - private IJsonSerializer _jsonSerializer; - private IAuthenticationRepository _authenticationRepository; - private IAuthorizationContext _authContext; - private IServerApplicationHost _appHost; - private ITaskManager _taskManager; + private readonly IServerConfigurationManager _config; + private readonly ILogger _logger; + private readonly IAuthenticationRepository _authenticationRepository; + private readonly IAuthorizationContext _authContext; + private readonly IServerApplicationHost _appHost; /// /// Initializes a new instance of the class. @@ -207,7 +203,7 @@ namespace Emby.Server.Implementations.QuickConnect scale = BitConverter.ToUInt32(raw, 0); } - int code = (int)(min + (max - min) * (scale / (double)uint.MaxValue)); + int code = (int)(min + ((max - min) * (scale / (double)uint.MaxValue))); return code.ToString(CultureInfo.InvariantCulture); } @@ -272,7 +268,26 @@ namespace Emby.Server.Implementations.QuickConnect return tokens.Count(); } + /// + /// Dispose. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + /// + /// Dispose. + /// + /// Dispose unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _rng?.Dispose(); + } + } private string GenerateSecureRandom(int length = 32) { var bytes = new byte[length]; diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index d7054e0b1..188b366aa 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1413,7 +1413,7 @@ namespace Emby.Server.Implementations.Session Limit = 1 }); - if(result.TotalRecordCount < 1) + if (result.TotalRecordCount < 1) { throw new SecurityException("Unknown quick connect token"); } -- cgit v1.2.3 From 9018f8d8be10bc4812f7d1bd230a1516eca61eea Mon Sep 17 00:00:00 2001 From: telans Date: Tue, 16 Jun 2020 10:37:52 +1200 Subject: Add full stop at end of comments (SA1629) --- Emby.Dlna/Didl/DidlBuilder.cs | 2 +- Emby.Dlna/DlnaManager.cs | 2 +- Emby.Dlna/PlayTo/Device.cs | 2 +- Emby.Naming/Common/MediaType.cs | 6 +- Emby.Server.Implementations/ApplicationHost.cs | 2 +- .../Data/BaseSqliteRepository.cs | 4 +- .../Data/SqliteDisplayPreferencesRepository.cs | 6 +- .../Data/SqliteItemRepository.cs | 10 +-- .../Data/SqliteUserDataRepository.cs | 6 +- Emby.Server.Implementations/Dto/DtoService.cs | 8 +-- .../HttpClientManager/HttpClientManager.cs | 2 +- .../HttpServer/FileWriter.cs | 4 +- .../HttpServer/HttpListenerHost.cs | 2 +- .../HttpServer/HttpResultFactory.cs | 2 +- .../HttpServer/RangeRequestWriter.cs | 8 +-- .../Library/CoreResolutionIgnoreRule.cs | 2 +- .../Library/IgnorePatterns.cs | 6 +- .../Library/LibraryManager.cs | 14 ++-- .../Library/ResolverHelper.cs | 2 +- .../Library/Resolvers/ItemResolver.cs | 2 +- .../Library/UserDataManager.cs | 4 +- .../LiveTv/RefreshChannelsScheduledTask.cs | 2 +- .../Networking/NetworkManager.cs | 2 +- .../ScheduledTasks/ScheduledTaskWorker.cs | 18 +++--- .../ScheduledTasks/TaskManager.cs | 8 +-- .../ScheduledTasks/Tasks/DeleteCacheFileTask.cs | 8 +-- .../Tasks/DeleteTranscodeFileTask.cs | 2 +- .../ScheduledTasks/Triggers/DailyTrigger.cs | 4 +- .../ScheduledTasks/Triggers/IntervalTrigger.cs | 6 +- .../ScheduledTasks/Triggers/StartupTrigger.cs | 4 +- .../ScheduledTasks/Triggers/WeeklyTrigger.cs | 8 +-- .../Services/ServiceHandler.cs | 2 +- .../Services/UrlExtensions.cs | 2 +- .../Session/SessionManager.cs | 2 +- .../Session/SessionWebSocketListener.cs | 6 +- .../Sorting/AlbumArtistComparer.cs | 2 +- .../Sorting/AlbumComparer.cs | 2 +- .../Sorting/CriticRatingComparer.cs | 2 +- .../Sorting/DateCreatedComparer.cs | 2 +- .../Sorting/DatePlayedComparer.cs | 2 +- .../Sorting/NameComparer.cs | 2 +- .../Sorting/PlayCountComparer.cs | 2 +- .../Sorting/PremiereDateComparer.cs | 2 +- .../Sorting/ProductionYearComparer.cs | 2 +- .../Sorting/RandomComparer.cs | 2 +- .../Sorting/RuntimeComparer.cs | 2 +- .../Sorting/SortNameComparer.cs | 2 +- Emby.Server.Implementations/Udp/UdpServer.cs | 2 +- Jellyfin.Data/Entities/Artwork.cs | 14 ++-- Jellyfin.Data/Entities/Book.cs | 2 +- Jellyfin.Data/Entities/BookMetadata.cs | 4 +- Jellyfin.Data/Entities/Chapter.cs | 20 +++--- Jellyfin.Data/Entities/Collection.cs | 10 +-- Jellyfin.Data/Entities/CollectionItem.cs | 10 +-- Jellyfin.Data/Entities/Company.cs | 8 +-- Jellyfin.Data/Entities/CompanyMetadata.cs | 10 +-- Jellyfin.Data/Entities/CustomItem.cs | 2 +- Jellyfin.Data/Entities/CustomItemMetadata.cs | 2 +- Jellyfin.Data/Entities/Episode.cs | 4 +- Jellyfin.Data/Entities/EpisodeMetadata.cs | 8 +-- Jellyfin.Data/Entities/Genre.cs | 10 +-- Jellyfin.Data/Entities/Library.cs | 10 +-- Jellyfin.Data/Entities/LibraryItem.cs | 16 ++--- Jellyfin.Data/Entities/LibraryRoot.cs | 16 ++--- Jellyfin.Data/Entities/MediaFile.cs | 16 ++--- Jellyfin.Data/Entities/MediaFileStream.cs | 12 ++-- Jellyfin.Data/Entities/Metadata.cs | 30 ++++----- Jellyfin.Data/Entities/MetadataProvider.cs | 10 +-- Jellyfin.Data/Entities/MetadataProviderId.cs | 12 ++-- Jellyfin.Data/Entities/Movie.cs | 2 +- Jellyfin.Data/Entities/MovieMetadata.cs | 10 +-- Jellyfin.Data/Entities/MusicAlbum.cs | 2 +- Jellyfin.Data/Entities/MusicAlbumMetadata.cs | 8 +-- Jellyfin.Data/Entities/Person.cs | 24 +++---- Jellyfin.Data/Entities/PersonRole.cs | 16 ++--- Jellyfin.Data/Entities/Photo.cs | 2 +- Jellyfin.Data/Entities/PhotoMetadata.cs | 2 +- Jellyfin.Data/Entities/ProviderMapping.cs | 6 +- Jellyfin.Data/Entities/Rating.cs | 14 ++-- Jellyfin.Data/Entities/RatingSource.cs | 20 +++--- Jellyfin.Data/Entities/Release.cs | 10 +-- Jellyfin.Data/Entities/Season.cs | 4 +- Jellyfin.Data/Entities/SeasonMetadata.cs | 4 +- Jellyfin.Data/Entities/Series.cs | 10 +-- Jellyfin.Data/Entities/SeriesMetadata.cs | 10 +-- Jellyfin.Data/Entities/Track.cs | 4 +- Jellyfin.Data/Entities/TrackMetadata.cs | 2 +- Jellyfin.Data/Enums/PreferenceKind.cs | 2 +- Jellyfin.Server.Implementations/JellyfinDb.cs | 2 +- MediaBrowser.Api/ApiEntryPoint.cs | 4 +- MediaBrowser.Api/BaseApiService.cs | 2 +- MediaBrowser.Api/ChannelService.cs | 6 +- MediaBrowser.Api/ConfigurationService.cs | 8 +-- MediaBrowser.Api/DisplayPreferencesService.cs | 8 +-- MediaBrowser.Api/EnvironmentService.cs | 12 ++-- MediaBrowser.Api/IHasItemFields.cs | 2 +- MediaBrowser.Api/Images/ImageByNameService.cs | 10 +-- MediaBrowser.Api/Images/ImageRequest.cs | 12 ++-- MediaBrowser.Api/Images/ImageService.cs | 20 +++--- MediaBrowser.Api/Images/RemoteImageService.cs | 2 +- MediaBrowser.Api/Library/LibraryService.cs | 14 ++-- .../Library/LibraryStructureService.cs | 8 +-- MediaBrowser.Api/LiveTv/LiveTvService.cs | 14 ++-- MediaBrowser.Api/LocalizationService.cs | 12 ++-- MediaBrowser.Api/Movies/MoviesService.cs | 4 +- MediaBrowser.Api/Movies/TrailersService.cs | 6 +- MediaBrowser.Api/Music/AlbumsService.cs | 6 +- MediaBrowser.Api/PackageService.cs | 10 +-- MediaBrowser.Api/Playback/BaseStreamingService.cs | 2 +- MediaBrowser.Api/Playback/Hls/BaseHlsService.cs | 2 +- MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs | 6 +- MediaBrowser.Api/Playback/Hls/VideoHlsService.cs | 2 +- .../Playback/Progressive/AudioService.cs | 4 +- .../Progressive/BaseProgressiveStreamingService.cs | 2 +- .../Playback/Progressive/VideoService.cs | 4 +- .../Playback/StaticRemoteStreamWriter.cs | 6 +- MediaBrowser.Api/Playback/StreamRequest.cs | 2 +- MediaBrowser.Api/PlaylistService.cs | 4 +- MediaBrowser.Api/PluginService.cs | 20 +++--- .../ScheduledTasks/ScheduledTaskService.cs | 12 ++-- .../ScheduledTasksWebSocketListener.cs | 2 +- MediaBrowser.Api/SearchService.cs | 10 +-- .../Sessions/SessionInfoWebSocketListener.cs | 4 +- MediaBrowser.Api/Sessions/SessionService.cs | 4 +- MediaBrowser.Api/SimilarItemsHelper.cs | 8 +-- MediaBrowser.Api/System/ActivityLogService.cs | 2 +- .../System/ActivityLogWebSocketListener.cs | 4 +- MediaBrowser.Api/System/SystemService.cs | 8 +-- MediaBrowser.Api/TvShowsService.cs | 22 +++---- MediaBrowser.Api/UserLibrary/ArtistsService.cs | 4 +- .../UserLibrary/BaseItemsByNameService.cs | 6 +- MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs | 22 +++---- MediaBrowser.Api/UserLibrary/GenresService.cs | 6 +- MediaBrowser.Api/UserLibrary/ItemsService.cs | 10 +-- MediaBrowser.Api/UserLibrary/PersonsService.cs | 6 +- MediaBrowser.Api/UserLibrary/PlaystateService.cs | 10 +-- MediaBrowser.Api/UserLibrary/StudiosService.cs | 6 +- MediaBrowser.Api/UserLibrary/UserLibraryService.cs | 20 +++--- MediaBrowser.Api/UserLibrary/YearsService.cs | 6 +- MediaBrowser.Api/UserService.cs | 24 +++---- .../Channels/InternalChannelFeatures.cs | 2 +- .../Configuration/IServerConfigurationManager.cs | 2 +- MediaBrowser.Controller/Drawing/IImageProcessor.cs | 2 +- MediaBrowser.Controller/Dto/IDtoService.cs | 2 +- .../Entities/AggregateFolder.cs | 2 +- MediaBrowser.Controller/Entities/Audio/Audio.cs | 2 +- .../Entities/Audio/MusicAlbum.cs | 2 +- .../Entities/Audio/MusicArtist.cs | 6 +- .../Entities/Audio/MusicGenre.cs | 6 +- MediaBrowser.Controller/Entities/BaseItem.cs | 28 ++++---- .../Entities/CollectionFolder.cs | 4 +- MediaBrowser.Controller/Entities/Extensions.cs | 2 +- MediaBrowser.Controller/Entities/Folder.cs | 12 ++-- MediaBrowser.Controller/Entities/Genre.cs | 6 +- .../Entities/ICollectionFolder.cs | 2 +- .../Entities/IHasAspectRatio.cs | 2 +- .../Entities/IHasDisplayOrder.cs | 2 +- .../Entities/IHasScreenshots.cs | 2 +- MediaBrowser.Controller/Entities/LinkedChild.cs | 2 +- MediaBrowser.Controller/Entities/Movies/BoxSet.cs | 2 +- MediaBrowser.Controller/Entities/Movies/Movie.cs | 2 +- MediaBrowser.Controller/Entities/Person.cs | 4 +- MediaBrowser.Controller/Entities/Studio.cs | 6 +- MediaBrowser.Controller/Entities/TV/Episode.cs | 4 +- MediaBrowser.Controller/Entities/TV/Season.cs | 6 +- MediaBrowser.Controller/Entities/TV/Series.cs | 4 +- MediaBrowser.Controller/Entities/Trailer.cs | 2 +- MediaBrowser.Controller/Entities/UserItemData.cs | 6 +- MediaBrowser.Controller/Entities/Video.cs | 2 +- MediaBrowser.Controller/Entities/Year.cs | 6 +- .../Extensions/StringExtensions.cs | 2 +- MediaBrowser.Controller/IServerApplicationHost.cs | 2 +- MediaBrowser.Controller/IServerApplicationPaths.cs | 16 ++--- MediaBrowser.Controller/Library/IIntroProvider.cs | 2 +- MediaBrowser.Controller/Library/ILibraryManager.cs | 14 ++-- .../Library/ILibraryPostScanTask.cs | 2 +- MediaBrowser.Controller/Library/IMetadataSaver.cs | 2 +- MediaBrowser.Controller/Library/ISearchEngine.cs | 2 +- .../Library/IUserDataManager.cs | 6 +- MediaBrowser.Controller/Library/IUserManager.cs | 2 +- .../Library/ItemChangeEventArgs.cs | 2 +- MediaBrowser.Controller/Library/ItemResolveArgs.cs | 2 +- .../Library/PlaybackProgressEventArgs.cs | 2 +- MediaBrowser.Controller/Library/Profiler.cs | 8 +-- MediaBrowser.Controller/Library/SearchHintInfo.cs | 2 +- MediaBrowser.Controller/Library/TVUtils.cs | 2 +- .../Library/UserDataSaveEventArgs.cs | 2 +- MediaBrowser.Controller/LiveTv/ChannelInfo.cs | 6 +- MediaBrowser.Controller/LiveTv/ILiveTvManager.cs | 2 +- MediaBrowser.Controller/LiveTv/LiveTvProgram.cs | 2 +- MediaBrowser.Controller/LiveTv/ProgramInfo.cs | 6 +- MediaBrowser.Controller/LiveTv/RecordingInfo.cs | 4 +- .../MediaEncoding/EncodingHelper.cs | 14 ++-- .../MediaEncoding/EncodingJobInfo.cs | 18 +++--- .../MediaEncoding/IMediaEncoder.cs | 2 +- .../MediaEncoding/MediaEncoderHelpers.cs | 2 +- .../Net/BasePeriodicWebSocketListener.cs | 10 +-- MediaBrowser.Controller/Net/IHttpResultFactory.cs | 2 +- MediaBrowser.Controller/Net/IHttpServer.cs | 6 +- MediaBrowser.Controller/Net/IWebSocketListener.cs | 2 +- .../Net/WebSocketMessageInfo.cs | 2 +- .../Persistence/IItemRepository.cs | 8 +-- MediaBrowser.Controller/Persistence/IRepository.cs | 4 +- .../Persistence/IUserDataRepository.cs | 6 +- .../Plugins/IPluginConfigurationPage.cs | 8 +-- .../Providers/IMetadataProvider.cs | 2 +- .../Providers/MetadataRefreshMode.cs | 8 +-- .../Providers/MetadataResult.cs | 2 +- .../Providers/VideoContentType.cs | 6 +- .../Resolvers/BaseItemResolver.cs | 4 +- MediaBrowser.Controller/Resolvers/IItemResolver.cs | 2 +- .../Resolvers/ResolverPriority.cs | 10 +-- MediaBrowser.Controller/Session/ISessionManager.cs | 8 +-- .../Sorting/IBaseItemComparer.cs | 2 +- .../Sorting/IUserBaseItemComparer.cs | 2 +- .../Sync/IRemoteSyncProvider.cs | 2 +- .../Parsers/BaseItemXmlParser.cs | 10 +-- .../BdInfo/BdInfoExaminer.cs | 2 +- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 4 +- .../Probing/FFProbeHelpers.cs | 8 +-- .../Probing/ProbeResultNormalizer.cs | 10 +-- .../Subtitles/SubtitleEncoder.cs | 2 +- MediaBrowser.Model/Channels/ChannelFeatures.cs | 2 +- MediaBrowser.Model/Channels/ChannelQuery.cs | 4 +- .../Configuration/BaseApplicationConfiguration.cs | 2 +- .../Configuration/MetadataPluginType.cs | 2 +- .../Configuration/ServerConfiguration.cs | 6 +- .../Configuration/UserConfiguration.cs | 2 +- MediaBrowser.Model/Dlna/AudioOptions.cs | 2 +- MediaBrowser.Model/Dlna/StreamInfo.cs | 26 ++++---- MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs | 8 +-- MediaBrowser.Model/Dto/BaseItemDto.cs | 2 +- MediaBrowser.Model/Dto/ImageOptions.cs | 2 +- MediaBrowser.Model/Dto/MediaSourceInfo.cs | 2 +- MediaBrowser.Model/Entities/DisplayPreferences.cs | 2 +- MediaBrowser.Model/Entities/MediaStream.cs | 2 +- MediaBrowser.Model/Entities/MetadataProvider.cs | 12 ++-- MediaBrowser.Model/Entities/VirtualFolderInfo.cs | 2 +- MediaBrowser.Model/IO/IZipClient.cs | 2 +- MediaBrowser.Model/LiveTv/ChannelType.cs | 2 +- MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs | 8 +-- MediaBrowser.Model/LiveTv/RecordingQuery.cs | 4 +- MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs | 2 +- MediaBrowser.Model/Net/NetworkShare.cs | 10 +-- .../Notifications/NotificationOption.cs | 2 +- MediaBrowser.Model/Querying/ItemFields.cs | 74 +++++++++++----------- MediaBrowser.Model/Querying/ItemSortBy.cs | 2 +- MediaBrowser.Model/Querying/NextUpQuery.cs | 4 +- MediaBrowser.Model/Querying/QueryResult.cs | 2 +- .../Querying/UpcomingEpisodesQuery.cs | 4 +- MediaBrowser.Model/Search/SearchQuery.cs | 4 +- MediaBrowser.Model/Services/ApiMemberAttribute.cs | 2 +- MediaBrowser.Model/Services/IRequest.cs | 8 +-- .../Services/IRequiresRequestStream.cs | 2 +- MediaBrowser.Model/Session/PlayRequest.cs | 4 +- MediaBrowser.Model/Sync/SyncCategory.cs | 6 +- MediaBrowser.Model/System/SystemInfo.cs | 2 +- MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs | 2 +- MediaBrowser.Model/Tasks/ITaskManager.cs | 2 +- MediaBrowser.Providers/Manager/ImageSaver.cs | 6 +- .../Manager/ItemImageProvider.cs | 4 +- MediaBrowser.Providers/Manager/ProviderManager.cs | 2 +- .../MediaInfo/AudioImageProvider.cs | 2 +- .../MediaInfo/FFProbeAudioInfo.cs | 2 +- .../MediaInfo/FFProbeVideoInfo.cs | 2 +- .../Plugins/Tmdb/Models/Search/MovieResult.cs | 2 +- .../Plugins/Tmdb/Movies/TmdbMovieProvider.cs | 4 +- .../TV/MissingEpisodeProvider.cs | 4 +- RSSDP/DiscoveredSsdpDevice.cs | 2 +- 269 files changed, 816 insertions(+), 816 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index 6ded76f7d..0f53bf2a4 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -705,7 +705,7 @@ namespace Emby.Dlna.Didl } /// - /// Adds fields used by both items and folders + /// Adds fields used by both items and folders. /// private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter) { diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index e5f483950..6b7063c5d 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -473,7 +473,7 @@ namespace Emby.Dlna /// /// Recreates the object using serialization, to ensure it's not a subclass. - /// If it's a subclass it may not serlialize properly to xml (different root element tag name) + /// If it's a subclass it may not serlialize properly to xml (different root element tag name). /// /// /// diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs index 86b72e264..6442ac5cc 100644 --- a/Emby.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -232,7 +232,7 @@ namespace Emby.Dlna.PlayTo } /// - /// Sets volume on a scale of 0-100 + /// Sets volume on a scale of 0-100. /// public async Task SetVolume(int value, CancellationToken cancellationToken) { diff --git a/Emby.Naming/Common/MediaType.cs b/Emby.Naming/Common/MediaType.cs index cc18ce4cd..148833765 100644 --- a/Emby.Naming/Common/MediaType.cs +++ b/Emby.Naming/Common/MediaType.cs @@ -5,17 +5,17 @@ namespace Emby.Naming.Common public enum MediaType { /// - /// The audio + /// The audio. /// Audio = 0, /// - /// The photo + /// The photo. /// Photo = 1, /// - /// The video + /// The video. /// Video = 2 } diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 5772dd479..23f0571a1 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -956,7 +956,7 @@ namespace Emby.Server.Implementations } /// - /// Notifies that the kernel that a change has been made that requires a restart + /// Notifies that the kernel that a change has been made that requires a restart. /// public void NotifyPendingRestart() { diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 53c9ccdbf..8a3716380 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -247,12 +247,12 @@ namespace Emby.Server.Implementations.Data public enum SynchronousMode { /// - /// SQLite continues without syncing as soon as it has handed data off to the operating system + /// SQLite continues without syncing as soon as it has handed data off to the operating system. /// Off = 0, /// - /// SQLite database engine will still sync at the most critical moments + /// SQLite database engine will still sync at the most critical moments. /// Normal = 1, diff --git a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs index 63d0321b7..5597155a8 100644 --- a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs @@ -59,7 +59,7 @@ namespace Emby.Server.Implementations.Data } /// - /// Opens the connection to the database + /// Opens the connection to the database. /// /// Task. private void InitializeInternal() @@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Data } /// - /// Save the display preferences associated with an item in the repo + /// Save the display preferences associated with an item in the repo. /// /// The display preferences. /// The user id. @@ -122,7 +122,7 @@ namespace Emby.Server.Implementations.Data } /// - /// Save all display preferences associated with a user in the repo + /// Save all display preferences associated with a user in the repo. /// /// The display preferences. /// The user id. diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index d33125661..13b581fc0 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -102,7 +102,7 @@ namespace Emby.Server.Implementations.Data protected override TempStoreMode TempStore => TempStoreMode.Memory; /// - /// Opens the connection to the database + /// Opens the connection to the database. /// public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager) { @@ -548,7 +548,7 @@ namespace Emby.Server.Implementations.Data } /// - /// Save a standard item in the repo + /// Save a standard item in the repo. /// /// The item. /// The cancellation token. @@ -1204,7 +1204,7 @@ namespace Emby.Server.Implementations.Data } /// - /// Internal retrieve from items or users table + /// Internal retrieve from items or users table. /// /// The id. /// BaseItem. @@ -1918,7 +1918,7 @@ namespace Emby.Server.Implementations.Data } /// - /// Gets chapters for an item + /// Gets chapters for an item. /// /// The item. /// IEnumerable{ChapterInfo}. @@ -1946,7 +1946,7 @@ namespace Emby.Server.Implementations.Data } /// - /// Gets a single chapter for an item + /// Gets a single chapter for an item. /// /// The item. /// The index. diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 7e66fa072..663c226a2 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -235,7 +235,7 @@ namespace Emby.Server.Implementations.Data } /// - /// Persist all user data for the specified user + /// Persist all user data for the specified user. /// private void PersistAllUserData(long internalUserId, UserItemData[] userDataList, CancellationToken cancellationToken) { @@ -309,7 +309,7 @@ namespace Emby.Server.Implementations.Data } /// - /// Return all user-data associated with the given user + /// Return all user-data associated with the given user. /// /// /// @@ -339,7 +339,7 @@ namespace Emby.Server.Implementations.Data } /// - /// Read a row from the specified reader into the provided userData object + /// Read a row from the specified reader into the provided userData object. /// /// private UserItemData ReadRow(IReadOnlyList reader) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 41ff7e3ab..c3bb7a814 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -74,7 +74,7 @@ namespace Emby.Server.Implementations.Dto } /// - /// Converts a BaseItem to a DTOBaseItem + /// Converts a BaseItem to a DTOBaseItem. /// /// The item. /// The fields. @@ -442,7 +442,7 @@ namespace Emby.Server.Implementations.Dto } /// - /// Gets client-side Id of a server-side BaseItem + /// Gets client-side Id of a server-side BaseItem. /// /// The item. /// System.String. @@ -537,7 +537,7 @@ namespace Emby.Server.Implementations.Dto } /// - /// Attaches People DTO's to a DTOBaseItem + /// Attaches People DTO's to a DTOBaseItem. /// /// The dto. /// The item. @@ -725,7 +725,7 @@ namespace Emby.Server.Implementations.Dto } /// - /// Sets simple property values on a DTOBaseItem + /// Sets simple property values on a DTOBaseItem. /// /// The dto. /// The item. diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs index 87977494a..25adc5812 100644 --- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs +++ b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs @@ -140,7 +140,7 @@ namespace Emby.Server.Implementations.HttpClientManager => SendAsync(options, HttpMethod.Get); /// - /// Performs a GET request and returns the resulting stream + /// Performs a GET request and returns the resulting stream. /// /// The options. /// Task{Stream}. diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs index 0b61e40b0..590eee1b4 100644 --- a/Emby.Server.Implementations/HttpServer/FileWriter.cs +++ b/Emby.Server.Implementations/HttpServer/FileWriter.cs @@ -32,12 +32,12 @@ namespace Emby.Server.Implementations.HttpServer private readonly IFileSystem _fileSystem; /// - /// The _options + /// The _options. /// private readonly IDictionary _options = new Dictionary(); /// - /// The _requested ranges + /// The _requested ranges. /// private List> _requestedRanges; diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 1b6e4b554..ca8cb4087 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -591,7 +591,7 @@ namespace Emby.Server.Implementations.HttpServer } /// - /// Get the default CORS headers + /// Get the default CORS headers. /// /// /// diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index d254d394f..ad31b3e1e 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -692,7 +692,7 @@ namespace Emby.Server.Implementations.HttpServer /// - /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that + /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that. /// /// The date. /// DateTime. diff --git a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs index 8b9028f6b..d83869077 100644 --- a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs +++ b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs @@ -34,17 +34,17 @@ namespace Emby.Server.Implementations.HttpServer private const int BufferSize = 81920; /// - /// The _options + /// The _options. /// private readonly Dictionary _options = new Dictionary(); /// - /// The us culture + /// The us culture. /// private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); /// - /// Additional HTTP Headers + /// Additional HTTP Headers. /// /// The headers. public IDictionary Headers => _options; @@ -110,7 +110,7 @@ namespace Emby.Server.Implementations.HttpServer } /// - /// The _requested ranges + /// The _requested ranges. /// private List> _requestedRanges; /// diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index 218e5a0c6..e140009ea 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -8,7 +8,7 @@ using MediaBrowser.Model.IO; namespace Emby.Server.Implementations.Library { /// - /// Provides the core resolver ignore rules + /// Provides the core resolver ignore rules. /// public class CoreResolutionIgnoreRule : IResolverIgnoreRule { diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index d12b5855b..8c4098948 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -4,12 +4,12 @@ using DotNet.Globbing; namespace Emby.Server.Implementations.Library { /// - /// Glob patterns for files to ignore + /// Glob patterns for files to ignore. /// public static class IgnorePatterns { /// - /// Files matching these glob patterns will be ignored + /// Files matching these glob patterns will be ignored. /// public static readonly string[] Patterns = new string[] { @@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.Library private static readonly Glob[] _globs = Patterns.Select(p => Glob.Parse(p, _globOptions)).ToArray(); /// - /// Returns true if the supplied path should be ignored + /// Returns true if the supplied path should be ignored. /// public static bool ShouldIgnore(string path) { diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 1d4651da2..799ce28a0 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -97,13 +97,13 @@ namespace Emby.Server.Implementations.Library private IIntroProvider[] IntroProviders { get; set; } /// - /// Gets or sets the list of entity resolution ignore rules + /// Gets or sets the list of entity resolution ignore rules. /// /// The entity resolution ignore rules. private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } /// - /// Gets or sets the list of currently registered entity resolvers + /// Gets or sets the list of currently registered entity resolvers. /// /// The entity resolvers enumerable. private IItemResolver[] EntityResolvers { get; set; } @@ -209,12 +209,12 @@ namespace Emby.Server.Implementations.Library } /// - /// The _root folder + /// The _root folder. /// private volatile AggregateFolder _rootFolder; /// - /// The _root folder sync lock + /// The _root folder sync lock. /// private readonly object _rootFolderSyncLock = new object(); @@ -627,7 +627,7 @@ namespace Emby.Server.Implementations.Library } /// - /// Determines whether a path should be ignored based on its contents - called after the contents have been read + /// Determines whether a path should be ignored based on its contents - called after the contents have been read. /// /// The args. /// true if XXXX, false otherwise @@ -909,7 +909,7 @@ namespace Emby.Server.Implementations.Library } /// - /// Gets a Genre + /// Gets a Genre. /// /// The name. /// Task{Genre}. @@ -990,7 +990,7 @@ namespace Emby.Server.Implementations.Library } /// - /// Reloads the root media folder + /// Reloads the root media folder. /// /// The progress. /// The cancellation token. diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs index 7ca15b4e5..4e4cac75b 100644 --- a/Emby.Server.Implementations/Library/ResolverHelper.cs +++ b/Emby.Server.Implementations/Library/ResolverHelper.cs @@ -107,7 +107,7 @@ namespace Emby.Server.Implementations.Library } /// - /// Ensures DateCreated and DateModified have values + /// Ensures DateCreated and DateModified have values. /// /// The file system. /// The item. diff --git a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs index 32ccc7fdd..9ca76095b 100644 --- a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs @@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Library.Resolvers public virtual ResolverPriority Priority => ResolverPriority.First; /// - /// Sets initial values on the newly resolved item + /// Sets initial values on the newly resolved item. /// /// The item. /// The args. diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index 9e17fa672..175b3af57 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -103,7 +103,7 @@ namespace Emby.Server.Implementations.Library } /// - /// Retrieve all user data for the given user + /// Retrieve all user data for the given user. /// /// /// @@ -188,7 +188,7 @@ namespace Emby.Server.Implementations.Library } /// - /// Converts a UserItemData to a DTOUserItemData + /// Converts a UserItemData to a DTOUserItemData. /// /// The data. /// DtoUserItemData. diff --git a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs index 8e7d60a15..f1b61f7c7 100644 --- a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs +++ b/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs @@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.LiveTv } /// - /// Creates the triggers that define when the task will run + /// Creates the triggers that define when the task will run. /// /// IEnumerable{BaseTaskTrigger}. public IEnumerable GetDefaultTriggers() diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs index 2909cdd0d..d12b5a937 100644 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ b/Emby.Server.Implementations/Networking/NetworkManager.cs @@ -411,7 +411,7 @@ namespace Emby.Server.Implementations.Networking } /// - /// Gets a random port number that is currently available + /// Gets a random port number that is currently available. /// /// System.Int32. public int GetRandomUnusedTcpPort() diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index e58c335a8..1995d1309 100644 --- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -18,7 +18,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.ScheduledTasks { /// - /// Class ScheduledTaskWorker + /// Class ScheduledTaskWorker. /// public class ScheduledTaskWorker : IScheduledTaskWorker { @@ -111,11 +111,11 @@ namespace Emby.Server.Implementations.ScheduledTasks private bool _readFromFile = false; /// - /// The _last execution result + /// The _last execution result. /// private TaskResult _lastExecutionResult; /// - /// The _last execution result sync lock + /// The _last execution result sync lock. /// private readonly object _lastExecutionResultSyncLock = new object(); /// @@ -182,7 +182,7 @@ namespace Emby.Server.Implementations.ScheduledTasks public string Category => ScheduledTask.Category; /// - /// Gets the current cancellation token + /// Gets the current cancellation token. /// /// The current cancellation token source. private CancellationTokenSource CurrentCancellationTokenSource { get; set; } @@ -278,7 +278,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// - /// The _id + /// The _id. /// private string _id; @@ -358,7 +358,7 @@ namespace Emby.Server.Implementations.ScheduledTasks private Task _currentTask; /// - /// Executes the task + /// Executes the task. /// /// Task options. /// Task. @@ -453,7 +453,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// - /// Stops the task if it is currently executing + /// Stops the task if it is currently executing. /// /// Cannot cancel a Task unless it is in the Running state. public void Cancel() @@ -683,7 +683,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// - /// Converts a TaskTriggerInfo into a concrete BaseTaskTrigger + /// Converts a TaskTriggerInfo into a concrete BaseTaskTrigger. /// /// The info. /// BaseTaskTrigger. @@ -753,7 +753,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// - /// Disposes each trigger + /// Disposes each trigger. /// private void DisposeTriggers() { diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs index 94220ac5d..0ad4253e4 100644 --- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs +++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs @@ -15,7 +15,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.ScheduledTasks { /// - /// Class TaskManager + /// Class TaskManager. /// public class TaskManager : ITaskManager { @@ -23,13 +23,13 @@ namespace Emby.Server.Implementations.ScheduledTasks public event EventHandler TaskCompleted; /// - /// Gets the list of Scheduled Tasks + /// Gets the list of Scheduled Tasks. /// /// The scheduled tasks. public IScheduledTaskWorker[] ScheduledTasks { get; private set; } /// - /// The _task queue + /// The _task queue. /// private readonly ConcurrentQueue> _taskQueue = new ConcurrentQueue>(); @@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// - /// Cancels if running + /// Cancels if running. /// /// public void CancelIfRunning() diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs index 966b549b2..e29fcfb5f 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs @@ -13,7 +13,7 @@ using MediaBrowser.Model.Globalization; namespace Emby.Server.Implementations.ScheduledTasks.Tasks { /// - /// Deletes old cache files + /// Deletes old cache files. /// public class DeleteCacheFileTask : IScheduledTask, IConfigurableScheduledTask { @@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks } /// - /// Creates the triggers that define when the task will run + /// Creates the triggers that define when the task will run. /// /// IEnumerable{BaseTaskTrigger}. public IEnumerable GetDefaultTriggers() @@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks } /// - /// Returns the task to be executed + /// Returns the task to be executed. /// /// The cancellation token. /// The progress. @@ -93,7 +93,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// - /// Deletes the cache files from directory with a last write time less than a given date + /// Deletes the cache files from directory with a last write time less than a given date. /// /// The task cancellation token. /// The directory. diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs index 53cf9a0a5..691408167 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs @@ -13,7 +13,7 @@ using MediaBrowser.Model.Globalization; namespace Emby.Server.Implementations.ScheduledTasks.Tasks { /// - /// Deletes all transcoding temp files + /// Deletes all transcoding temp files. /// public class DeleteTranscodeFileTask : IScheduledTask, IConfigurableScheduledTask { diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs index c7819d4c0..eb628ec5f 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs @@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.ScheduledTasks private Timer Timer { get; set; } /// - /// Stars waiting for the trigger action + /// Stars waiting for the trigger action. /// /// The last result. /// The logger. @@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// - /// Stops waiting for the trigger action + /// Stops waiting for the trigger action. /// public void Stop() { diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs index 74cd4ef1e..247a6785a 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.ScheduledTasks { /// - /// Represents a task trigger that runs repeatedly on an interval + /// Represents a task trigger that runs repeatedly on an interval. /// public class IntervalTrigger : ITaskTrigger { @@ -31,7 +31,7 @@ namespace Emby.Server.Implementations.ScheduledTasks private DateTime _lastStartDate; /// - /// Stars waiting for the trigger action + /// Stars waiting for the trigger action. /// /// The last result. /// The logger. @@ -70,7 +70,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// - /// Stops waiting for the trigger action + /// Stops waiting for the trigger action. /// public void Stop() { diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs index e171a9e9f..96e5d8897 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs @@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// - /// Stars waiting for the trigger action + /// Stars waiting for the trigger action. /// /// The last result. /// The logger. @@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// - /// Stops waiting for the trigger action + /// Stops waiting for the trigger action. /// public void Stop() { diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs index ad0b57af6..4f1bf5c19 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs @@ -6,12 +6,12 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.ScheduledTasks { /// - /// Represents a task trigger that fires on a weekly basis + /// Represents a task trigger that fires on a weekly basis. /// public class WeeklyTrigger : ITaskTrigger { /// - /// Get the time of day to trigger the task to run + /// Get the time of day to trigger the task to run. /// /// The time of day. public TimeSpan TimeOfDay { get; set; } @@ -34,7 +34,7 @@ namespace Emby.Server.Implementations.ScheduledTasks private Timer Timer { get; set; } /// - /// Stars waiting for the trigger action + /// Stars waiting for the trigger action. /// /// The last result. /// The logger. @@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// - /// Stops waiting for the trigger action + /// Stops waiting for the trigger action. /// public void Stop() { diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs index 7f44357e1..a42f88ea0 100644 --- a/Emby.Server.Implementations/Services/ServiceHandler.cs +++ b/Emby.Server.Implementations/Services/ServiceHandler.cs @@ -180,7 +180,7 @@ namespace Emby.Server.Implementations.Services => string.Equals(method, expected, StringComparison.OrdinalIgnoreCase); /// - /// Duplicate params have their values joined together in a comma-delimited string + /// Duplicate params have their values joined together in a comma-delimited string. /// private static Dictionary GetFlattenedRequestParams(HttpRequest request) { diff --git a/Emby.Server.Implementations/Services/UrlExtensions.cs b/Emby.Server.Implementations/Services/UrlExtensions.cs index e3b6aa197..92e36b60e 100644 --- a/Emby.Server.Implementations/Services/UrlExtensions.cs +++ b/Emby.Server.Implementations/Services/UrlExtensions.cs @@ -9,7 +9,7 @@ namespace Emby.Server.Implementations.Services /// Donated by Ivan Korneliuk from his post: /// http://korneliuk.blogspot.com/2012/08/servicestack-reusing-dtos.html /// - /// Modified to only allow using routes matching the supplied HTTP Verb + /// Modified to only allow using routes matching the supplied HTTP Verb. /// public static class UrlExtensions { diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 07e443ef5..75fdedd10 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -843,7 +843,7 @@ namespace Emby.Server.Implementations.Session } /// - /// Used to report that playback has ended for an item + /// Used to report that playback has ended for an item. /// /// The info. /// Task. diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index ef32c692c..a891e03f9 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Session { /// - /// Class SessionWebSocketListener + /// Class SessionWebSocketListener. /// public sealed class SessionWebSocketListener : IWebSocketListener, IDisposable { @@ -34,12 +34,12 @@ namespace Emby.Server.Implementations.Session public const float ForceKeepAliveFactor = 0.75f; /// - /// The _session manager + /// The _session manager. /// private readonly ISessionManager _sessionManager; /// - /// The _logger + /// The _logger. /// private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; diff --git a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs index 0804b01fc..7657cc74e 100644 --- a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs +++ b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs @@ -8,7 +8,7 @@ using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { /// - /// Class AlbumArtistComparer + /// Class AlbumArtistComparer. /// public class AlbumArtistComparer : IBaseItemComparer { diff --git a/Emby.Server.Implementations/Sorting/AlbumComparer.cs b/Emby.Server.Implementations/Sorting/AlbumComparer.cs index 3831a0d2d..7dfdd9ecf 100644 --- a/Emby.Server.Implementations/Sorting/AlbumComparer.cs +++ b/Emby.Server.Implementations/Sorting/AlbumComparer.cs @@ -7,7 +7,7 @@ using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { /// - /// Class AlbumComparer + /// Class AlbumComparer. /// public class AlbumComparer : IBaseItemComparer { diff --git a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs index adb78dec5..fa136c36d 100644 --- a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs +++ b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs @@ -5,7 +5,7 @@ using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { /// - /// Class CriticRatingComparer + /// Class CriticRatingComparer. /// public class CriticRatingComparer : IBaseItemComparer { diff --git a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs index 8501bd9ee..ea981e840 100644 --- a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs +++ b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs @@ -6,7 +6,7 @@ using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { /// - /// Class DateCreatedComparer + /// Class DateCreatedComparer. /// public class DateCreatedComparer : IBaseItemComparer { diff --git a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs index 5e527ea25..16bd2aff8 100644 --- a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs @@ -8,7 +8,7 @@ using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { /// - /// Class DatePlayedComparer + /// Class DatePlayedComparer. /// public class DatePlayedComparer : IUserBaseItemComparer { diff --git a/Emby.Server.Implementations/Sorting/NameComparer.cs b/Emby.Server.Implementations/Sorting/NameComparer.cs index 4eb1549f5..da020d8d8 100644 --- a/Emby.Server.Implementations/Sorting/NameComparer.cs +++ b/Emby.Server.Implementations/Sorting/NameComparer.cs @@ -6,7 +6,7 @@ using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { /// - /// Class NameComparer + /// Class NameComparer. /// public class NameComparer : IBaseItemComparer { diff --git a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs index afbaaf6ab..5c2830322 100644 --- a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs +++ b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs @@ -7,7 +7,7 @@ using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { /// - /// Class PlayCountComparer + /// Class PlayCountComparer. /// public class PlayCountComparer : IUserBaseItemComparer { diff --git a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs index 0c944a7a0..a24dc4030 100644 --- a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs +++ b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs @@ -6,7 +6,7 @@ using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { /// - /// Class PremiereDateComparer + /// Class PremiereDateComparer. /// public class PremiereDateComparer : IBaseItemComparer { diff --git a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs index 472a07eb3..e2857df0b 100644 --- a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs +++ b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs @@ -5,7 +5,7 @@ using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { /// - /// Class ProductionYearComparer + /// Class ProductionYearComparer. /// public class ProductionYearComparer : IBaseItemComparer { diff --git a/Emby.Server.Implementations/Sorting/RandomComparer.cs b/Emby.Server.Implementations/Sorting/RandomComparer.cs index bde8b4534..7739d0418 100644 --- a/Emby.Server.Implementations/Sorting/RandomComparer.cs +++ b/Emby.Server.Implementations/Sorting/RandomComparer.cs @@ -6,7 +6,7 @@ using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { /// - /// Class RandomComparer + /// Class RandomComparer. /// public class RandomComparer : IBaseItemComparer { diff --git a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs index 1d2bdde26..f165123ea 100644 --- a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs +++ b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs @@ -6,7 +6,7 @@ using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { /// - /// Class RuntimeComparer + /// Class RuntimeComparer. /// public class RuntimeComparer : IBaseItemComparer { diff --git a/Emby.Server.Implementations/Sorting/SortNameComparer.cs b/Emby.Server.Implementations/Sorting/SortNameComparer.cs index cc0571c78..93389fc3e 100644 --- a/Emby.Server.Implementations/Sorting/SortNameComparer.cs +++ b/Emby.Server.Implementations/Sorting/SortNameComparer.cs @@ -6,7 +6,7 @@ using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { /// - /// Class SortNameComparer + /// Class SortNameComparer. /// public class SortNameComparer : IBaseItemComparer { diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index a26f714b1..bf8a436b4 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -18,7 +18,7 @@ namespace Emby.Server.Implementations.Udp public sealed class UdpServer : IDisposable { /// - /// The _logger + /// The _logger. /// private readonly ILogger _logger; private readonly IServerApplicationHost _appHost; diff --git a/Jellyfin.Data/Entities/Artwork.cs b/Jellyfin.Data/Entities/Artwork.cs index 214fb4cb1..891904cc3 100644 --- a/Jellyfin.Data/Entities/Artwork.cs +++ b/Jellyfin.Data/Entities/Artwork.cs @@ -24,7 +24,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// /// @@ -64,7 +64,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Id + /// Backing field for Id. /// internal int _Id; /// @@ -77,7 +77,7 @@ namespace Jellyfin.Data.Entities partial void GetId(ref int result); /// - /// Identity, Indexed, Required + /// Identity, Indexed, Required. /// [Key] [Required] @@ -101,7 +101,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Path + /// Backing field for Path. /// protected string _Path; /// @@ -139,7 +139,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Kind + /// Backing field for Kind. /// internal Enums.ArtKind _Kind; /// @@ -152,7 +152,7 @@ namespace Jellyfin.Data.Entities partial void GetKind(ref Enums.ArtKind result); /// - /// Indexed, Required + /// Indexed, Required. /// [Required] public Enums.ArtKind Kind @@ -175,7 +175,7 @@ namespace Jellyfin.Data.Entities } /// - /// Required, ConcurrenyToken + /// Required, ConcurrenyToken. /// [ConcurrencyCheck] [Required] diff --git a/Jellyfin.Data/Entities/Book.cs b/Jellyfin.Data/Entities/Book.cs index faefc7400..c4d12496e 100644 --- a/Jellyfin.Data/Entities/Book.cs +++ b/Jellyfin.Data/Entities/Book.cs @@ -28,7 +28,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// This is whats gets displayed in the Urls and API requests. This could also be a string. public Book(Guid urlid, DateTime dateadded) diff --git a/Jellyfin.Data/Entities/BookMetadata.cs b/Jellyfin.Data/Entities/BookMetadata.cs index dd389b64a..474f906a1 100644 --- a/Jellyfin.Data/Entities/BookMetadata.cs +++ b/Jellyfin.Data/Entities/BookMetadata.cs @@ -27,7 +27,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// The title or name of the object /// ISO-639-3 3-character language codes @@ -64,7 +64,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for ISBN + /// Backing field for ISBN. /// protected long? _ISBN; /// diff --git a/Jellyfin.Data/Entities/Chapter.cs b/Jellyfin.Data/Entities/Chapter.cs index 9b3a5e827..5a5cdaa8f 100644 --- a/Jellyfin.Data/Entities/Chapter.cs +++ b/Jellyfin.Data/Entities/Chapter.cs @@ -25,7 +25,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// ISO-639-3 3-character language codes /// @@ -60,7 +60,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Id + /// Backing field for Id. /// internal int _Id; /// @@ -73,7 +73,7 @@ namespace Jellyfin.Data.Entities partial void GetId(ref int result); /// - /// Identity, Indexed, Required + /// Identity, Indexed, Required. /// [Key] [Required] @@ -98,7 +98,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Name + /// Backing field for Name. /// protected string _Name; /// @@ -135,7 +135,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Language + /// Backing field for Language. /// protected string _Language; /// @@ -149,7 +149,7 @@ namespace Jellyfin.Data.Entities /// /// Required, Min length = 3, Max length = 3 - /// ISO-639-3 3-character language codes + /// ISO-639-3 3-character language codes. /// [Required] [MinLength(3)] @@ -175,7 +175,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for TimeStart + /// Backing field for TimeStart. /// protected long _TimeStart; /// @@ -188,7 +188,7 @@ namespace Jellyfin.Data.Entities partial void GetTimeStart(ref long result); /// - /// Required + /// Required. /// [Required] public long TimeStart @@ -211,7 +211,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for TimeEnd + /// Backing field for TimeEnd. /// protected long? _TimeEnd; /// @@ -243,7 +243,7 @@ namespace Jellyfin.Data.Entities } /// - /// Required, ConcurrenyToken + /// Required, ConcurrenyToken. /// [ConcurrencyCheck] [Required] diff --git a/Jellyfin.Data/Entities/Collection.cs b/Jellyfin.Data/Entities/Collection.cs index c040cfe33..87c9487c0 100644 --- a/Jellyfin.Data/Entities/Collection.cs +++ b/Jellyfin.Data/Entities/Collection.cs @@ -9,7 +9,7 @@ namespace Jellyfin.Data.Entities partial void Init(); /// - /// Default constructor + /// Default constructor. /// public Collection() { @@ -23,7 +23,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Id + /// Backing field for Id. /// internal int _Id; /// @@ -36,7 +36,7 @@ namespace Jellyfin.Data.Entities partial void GetId(ref int result); /// - /// Identity, Indexed, Required + /// Identity, Indexed, Required. /// [Key] [Required] @@ -61,7 +61,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Name + /// Backing field for Name. /// protected string _Name; /// @@ -98,7 +98,7 @@ namespace Jellyfin.Data.Entities } /// - /// Required, ConcurrenyToken + /// Required, ConcurrenyToken. /// [ConcurrencyCheck] [Required] diff --git a/Jellyfin.Data/Entities/CollectionItem.cs b/Jellyfin.Data/Entities/CollectionItem.cs index c5e54c3a2..fc3705fe0 100644 --- a/Jellyfin.Data/Entities/CollectionItem.cs +++ b/Jellyfin.Data/Entities/CollectionItem.cs @@ -28,7 +28,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// /// @@ -67,7 +67,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Id + /// Backing field for Id. /// internal int _Id; /// @@ -80,7 +80,7 @@ namespace Jellyfin.Data.Entities partial void GetId(ref int result); /// - /// Identity, Indexed, Required + /// Identity, Indexed, Required. /// [Key] [Required] @@ -105,7 +105,7 @@ namespace Jellyfin.Data.Entities } /// - /// Required, ConcurrenyToken + /// Required, ConcurrenyToken. /// [ConcurrencyCheck] [Required] @@ -121,7 +121,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Required + /// Required. /// [ForeignKey("LibraryItem_Id")] public virtual LibraryItem LibraryItem { get; set; } diff --git a/Jellyfin.Data/Entities/Company.cs b/Jellyfin.Data/Entities/Company.cs index 7d6f3b207..2af05b8a1 100644 --- a/Jellyfin.Data/Entities/Company.cs +++ b/Jellyfin.Data/Entities/Company.cs @@ -28,7 +28,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// /// @@ -75,7 +75,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Id + /// Backing field for Id. /// internal int _Id; /// @@ -88,7 +88,7 @@ namespace Jellyfin.Data.Entities partial void GetId(ref int result); /// - /// Identity, Indexed, Required + /// Identity, Indexed, Required. /// [Key] [Required] @@ -113,7 +113,7 @@ namespace Jellyfin.Data.Entities } /// - /// Required, ConcurrenyToken + /// Required, ConcurrenyToken. /// [ConcurrencyCheck] [Required] diff --git a/Jellyfin.Data/Entities/CompanyMetadata.cs b/Jellyfin.Data/Entities/CompanyMetadata.cs index 1ad03b4f9..64d59fbd2 100644 --- a/Jellyfin.Data/Entities/CompanyMetadata.cs +++ b/Jellyfin.Data/Entities/CompanyMetadata.cs @@ -24,7 +24,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// The title or name of the object /// ISO-639-3 3-character language codes @@ -60,7 +60,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Description + /// Backing field for Description. /// protected string _Description; /// @@ -97,7 +97,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Headquarters + /// Backing field for Headquarters. /// protected string _Headquarters; /// @@ -134,7 +134,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Country + /// Backing field for Country. /// protected string _Country; /// @@ -171,7 +171,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Homepage + /// Backing field for Homepage. /// protected string _Homepage; /// diff --git a/Jellyfin.Data/Entities/CustomItem.cs b/Jellyfin.Data/Entities/CustomItem.cs index 5f6fc3a23..446391591 100644 --- a/Jellyfin.Data/Entities/CustomItem.cs +++ b/Jellyfin.Data/Entities/CustomItem.cs @@ -28,7 +28,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// This is whats gets displayed in the Urls and API requests. This could also be a string. public CustomItem(Guid urlid, DateTime dateadded) diff --git a/Jellyfin.Data/Entities/CustomItemMetadata.cs b/Jellyfin.Data/Entities/CustomItemMetadata.cs index ee37aaaa9..b81408aa6 100644 --- a/Jellyfin.Data/Entities/CustomItemMetadata.cs +++ b/Jellyfin.Data/Entities/CustomItemMetadata.cs @@ -23,7 +23,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// The title or name of the object /// ISO-639-3 3-character language codes diff --git a/Jellyfin.Data/Entities/Episode.cs b/Jellyfin.Data/Entities/Episode.cs index 88531205f..132af9bdc 100644 --- a/Jellyfin.Data/Entities/Episode.cs +++ b/Jellyfin.Data/Entities/Episode.cs @@ -31,7 +31,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// This is whats gets displayed in the Urls and API requests. This could also be a string. /// @@ -66,7 +66,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for EpisodeNumber + /// Backing field for EpisodeNumber. /// protected int? _EpisodeNumber; /// diff --git a/Jellyfin.Data/Entities/EpisodeMetadata.cs b/Jellyfin.Data/Entities/EpisodeMetadata.cs index 0aa4b4270..a4e50fca2 100644 --- a/Jellyfin.Data/Entities/EpisodeMetadata.cs +++ b/Jellyfin.Data/Entities/EpisodeMetadata.cs @@ -24,7 +24,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// The title or name of the object /// ISO-639-3 3-character language codes @@ -60,7 +60,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Outline + /// Backing field for Outline. /// protected string _Outline; /// @@ -97,7 +97,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Plot + /// Backing field for Plot. /// protected string _Plot; /// @@ -134,7 +134,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Tagline + /// Backing field for Tagline. /// protected string _Tagline; /// diff --git a/Jellyfin.Data/Entities/Genre.cs b/Jellyfin.Data/Entities/Genre.cs index ff0710671..a38c019fb 100644 --- a/Jellyfin.Data/Entities/Genre.cs +++ b/Jellyfin.Data/Entities/Genre.cs @@ -25,7 +25,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// /// @@ -56,7 +56,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Id + /// Backing field for Id. /// internal int _Id; /// @@ -69,7 +69,7 @@ namespace Jellyfin.Data.Entities partial void GetId(ref int result); /// - /// Identity, Indexed, Required + /// Identity, Indexed, Required. /// [Key] [Required] @@ -94,7 +94,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Name + /// Backing field for Name. /// internal string _Name; /// @@ -132,7 +132,7 @@ namespace Jellyfin.Data.Entities } /// - /// Required, ConcurrenyToken + /// Required, ConcurrenyToken. /// [ConcurrencyCheck] [Required] diff --git a/Jellyfin.Data/Entities/Library.cs b/Jellyfin.Data/Entities/Library.cs index a5cc5c8da..c5609d858 100644 --- a/Jellyfin.Data/Entities/Library.cs +++ b/Jellyfin.Data/Entities/Library.cs @@ -25,7 +25,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// public Library(string name) @@ -51,7 +51,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Id + /// Backing field for Id. /// internal int _Id; /// @@ -64,7 +64,7 @@ namespace Jellyfin.Data.Entities partial void GetId(ref int result); /// - /// Identity, Indexed, Required + /// Identity, Indexed, Required. /// [Key] [Required] @@ -89,7 +89,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Name + /// Backing field for Name. /// protected string _Name; /// @@ -127,7 +127,7 @@ namespace Jellyfin.Data.Entities } /// - /// Required, ConcurrenyToken + /// Required, ConcurrenyToken. /// [ConcurrencyCheck] [Required] diff --git a/Jellyfin.Data/Entities/LibraryItem.cs b/Jellyfin.Data/Entities/LibraryItem.cs index c2ba7059d..b7dad8d7c 100644 --- a/Jellyfin.Data/Entities/LibraryItem.cs +++ b/Jellyfin.Data/Entities/LibraryItem.cs @@ -17,7 +17,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// This is whats gets displayed in the Urls and API requests. This could also be a string. protected LibraryItem(Guid urlid, DateTime dateadded) @@ -33,7 +33,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Id + /// Backing field for Id. /// internal int _Id; /// @@ -46,7 +46,7 @@ namespace Jellyfin.Data.Entities partial void GetId(ref int result); /// - /// Identity, Indexed, Required + /// Identity, Indexed, Required. /// [Key] [Required] @@ -71,7 +71,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for UrlId + /// Backing field for UrlId. /// internal Guid _UrlId; /// @@ -108,7 +108,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for DateAdded + /// Backing field for DateAdded. /// protected DateTime _DateAdded; /// @@ -121,7 +121,7 @@ namespace Jellyfin.Data.Entities partial void GetDateAdded(ref DateTime result); /// - /// Required + /// Required. /// [Required] public DateTime DateAdded @@ -144,7 +144,7 @@ namespace Jellyfin.Data.Entities } /// - /// Required, ConcurrenyToken + /// Required, ConcurrenyToken. /// [ConcurrencyCheck] [Required] @@ -160,7 +160,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Required + /// Required. /// [ForeignKey("LibraryRoot_Id")] public virtual LibraryRoot LibraryRoot { get; set; } diff --git a/Jellyfin.Data/Entities/LibraryRoot.cs b/Jellyfin.Data/Entities/LibraryRoot.cs index 7823db02a..a84c7de4b 100644 --- a/Jellyfin.Data/Entities/LibraryRoot.cs +++ b/Jellyfin.Data/Entities/LibraryRoot.cs @@ -25,7 +25,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// Absolute Path public LibraryRoot(string path) @@ -51,7 +51,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Id + /// Backing field for Id. /// internal int _Id; /// @@ -64,7 +64,7 @@ namespace Jellyfin.Data.Entities partial void GetId(ref int result); /// - /// Identity, Indexed, Required + /// Identity, Indexed, Required. /// [Key] [Required] @@ -89,7 +89,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Path + /// Backing field for Path. /// protected string _Path; /// @@ -103,7 +103,7 @@ namespace Jellyfin.Data.Entities /// /// Required, Max length = 65535 - /// Absolute Path + /// Absolute Path. /// [Required] [MaxLength(65535)] @@ -128,7 +128,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for NetworkPath + /// Backing field for NetworkPath. /// protected string _NetworkPath; /// @@ -166,7 +166,7 @@ namespace Jellyfin.Data.Entities } /// - /// Required, ConcurrenyToken + /// Required, ConcurrenyToken. /// [ConcurrencyCheck] [Required] @@ -182,7 +182,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Required + /// Required. /// [ForeignKey("Library_Id")] public virtual Library Library { get; set; } diff --git a/Jellyfin.Data/Entities/MediaFile.cs b/Jellyfin.Data/Entities/MediaFile.cs index 94c39a28a..d8781bcb2 100644 --- a/Jellyfin.Data/Entities/MediaFile.cs +++ b/Jellyfin.Data/Entities/MediaFile.cs @@ -28,7 +28,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// Relative to the LibraryRoot /// @@ -64,7 +64,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Id + /// Backing field for Id. /// internal int _Id; /// @@ -77,7 +77,7 @@ namespace Jellyfin.Data.Entities partial void GetId(ref int result); /// - /// Identity, Indexed, Required + /// Identity, Indexed, Required. /// [Key] [Required] @@ -102,7 +102,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Path + /// Backing field for Path. /// protected string _Path; /// @@ -116,7 +116,7 @@ namespace Jellyfin.Data.Entities /// /// Required, Max length = 65535 - /// Relative to the LibraryRoot + /// Relative to the LibraryRoot. /// [Required] [MaxLength(65535)] @@ -141,7 +141,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Kind + /// Backing field for Kind. /// protected Enums.MediaFileKind _Kind; /// @@ -154,7 +154,7 @@ namespace Jellyfin.Data.Entities partial void GetKind(ref Enums.MediaFileKind result); /// - /// Required + /// Required. /// [Required] public Enums.MediaFileKind Kind @@ -177,7 +177,7 @@ namespace Jellyfin.Data.Entities } /// - /// Required, ConcurrenyToken + /// Required, ConcurrenyToken. /// [ConcurrencyCheck] [Required] diff --git a/Jellyfin.Data/Entities/MediaFileStream.cs b/Jellyfin.Data/Entities/MediaFileStream.cs index 723977fdf..a7a33e255 100644 --- a/Jellyfin.Data/Entities/MediaFileStream.cs +++ b/Jellyfin.Data/Entities/MediaFileStream.cs @@ -25,7 +25,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// /// @@ -55,7 +55,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Id + /// Backing field for Id. /// internal int _Id; /// @@ -68,7 +68,7 @@ namespace Jellyfin.Data.Entities partial void GetId(ref int result); /// - /// Identity, Indexed, Required + /// Identity, Indexed, Required. /// [Key] [Required] @@ -93,7 +93,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for StreamNumber + /// Backing field for StreamNumber. /// protected int _StreamNumber; /// @@ -106,7 +106,7 @@ namespace Jellyfin.Data.Entities partial void GetStreamNumber(ref int result); /// - /// Required + /// Required. /// [Required] public int StreamNumber @@ -129,7 +129,7 @@ namespace Jellyfin.Data.Entities } /// - /// Required, ConcurrenyToken + /// Required, ConcurrenyToken. /// [ConcurrencyCheck] [Required] diff --git a/Jellyfin.Data/Entities/Metadata.cs b/Jellyfin.Data/Entities/Metadata.cs index 6558642cf..4282bb9f7 100644 --- a/Jellyfin.Data/Entities/Metadata.cs +++ b/Jellyfin.Data/Entities/Metadata.cs @@ -24,7 +24,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// The title or name of the object /// ISO-639-3 3-character language codes @@ -50,7 +50,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Id + /// Backing field for Id. /// internal int _Id; /// @@ -63,7 +63,7 @@ namespace Jellyfin.Data.Entities partial void GetId(ref int result); /// - /// Identity, Indexed, Required + /// Identity, Indexed, Required. /// [Key] [Required] @@ -88,7 +88,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Title + /// Backing field for Title. /// protected string _Title; /// @@ -102,7 +102,7 @@ namespace Jellyfin.Data.Entities /// /// Required, Max length = 1024 - /// The title or name of the object + /// The title or name of the object. /// [Required] [MaxLength(1024)] @@ -127,7 +127,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for OriginalTitle + /// Backing field for OriginalTitle. /// protected string _OriginalTitle; /// @@ -164,7 +164,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for SortTitle + /// Backing field for SortTitle. /// protected string _SortTitle; /// @@ -201,7 +201,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Language + /// Backing field for Language. /// protected string _Language; /// @@ -215,7 +215,7 @@ namespace Jellyfin.Data.Entities /// /// Required, Min length = 3, Max length = 3 - /// ISO-639-3 3-character language codes + /// ISO-639-3 3-character language codes. /// [Required] [MinLength(3)] @@ -241,7 +241,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for ReleaseDate + /// Backing field for ReleaseDate. /// protected DateTimeOffset? _ReleaseDate; /// @@ -273,7 +273,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for DateAdded + /// Backing field for DateAdded. /// protected DateTime _DateAdded; /// @@ -286,7 +286,7 @@ namespace Jellyfin.Data.Entities partial void GetDateAdded(ref DateTime result); /// - /// Required + /// Required. /// [Required] public DateTime DateAdded @@ -309,7 +309,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for DateModified + /// Backing field for DateModified. /// protected DateTime _DateModified; /// @@ -322,7 +322,7 @@ namespace Jellyfin.Data.Entities partial void GetDateModified(ref DateTime result); /// - /// Required + /// Required. /// [Required] public DateTime DateModified @@ -345,7 +345,7 @@ namespace Jellyfin.Data.Entities } /// - /// Required, ConcurrenyToken + /// Required, ConcurrenyToken. /// [ConcurrencyCheck] [Required] diff --git a/Jellyfin.Data/Entities/MetadataProvider.cs b/Jellyfin.Data/Entities/MetadataProvider.cs index bf9689709..9069d6ad6 100644 --- a/Jellyfin.Data/Entities/MetadataProvider.cs +++ b/Jellyfin.Data/Entities/MetadataProvider.cs @@ -25,7 +25,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// public MetadataProvider(string name) @@ -51,7 +51,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Id + /// Backing field for Id. /// internal int _Id; /// @@ -64,7 +64,7 @@ namespace Jellyfin.Data.Entities partial void GetId(ref int result); /// - /// Identity, Indexed, Required + /// Identity, Indexed, Required. /// [Key] [Required] @@ -89,7 +89,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Name + /// Backing field for Name. /// protected string _Name; /// @@ -127,7 +127,7 @@ namespace Jellyfin.Data.Entities } /// - /// Required, ConcurrenyToken + /// Required, ConcurrenyToken. /// [ConcurrencyCheck] [Required] diff --git a/Jellyfin.Data/Entities/MetadataProviderId.cs b/Jellyfin.Data/Entities/MetadataProviderId.cs index c49c6f42e..5facf1188 100644 --- a/Jellyfin.Data/Entities/MetadataProviderId.cs +++ b/Jellyfin.Data/Entities/MetadataProviderId.cs @@ -28,7 +28,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// /// @@ -77,7 +77,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Id + /// Backing field for Id. /// internal int _Id; /// @@ -90,7 +90,7 @@ namespace Jellyfin.Data.Entities partial void GetId(ref int result); /// - /// Identity, Indexed, Required + /// Identity, Indexed, Required. /// [Key] [Required] @@ -115,7 +115,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for ProviderId + /// Backing field for ProviderId. /// protected string _ProviderId; /// @@ -153,7 +153,7 @@ namespace Jellyfin.Data.Entities } /// - /// Required, ConcurrenyToken + /// Required, ConcurrenyToken. /// [ConcurrencyCheck] [Required] @@ -169,7 +169,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Required + /// Required. /// [ForeignKey("MetadataProvider_Id")] public virtual MetadataProvider MetadataProvider { get; set; } diff --git a/Jellyfin.Data/Entities/Movie.cs b/Jellyfin.Data/Entities/Movie.cs index ad2504b0d..64326ca3a 100644 --- a/Jellyfin.Data/Entities/Movie.cs +++ b/Jellyfin.Data/Entities/Movie.cs @@ -28,7 +28,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// This is whats gets displayed in the Urls and API requests. This could also be a string. public Movie(Guid urlid, DateTime dateadded) diff --git a/Jellyfin.Data/Entities/MovieMetadata.cs b/Jellyfin.Data/Entities/MovieMetadata.cs index 1f8f1c2a0..f01b208bc 100644 --- a/Jellyfin.Data/Entities/MovieMetadata.cs +++ b/Jellyfin.Data/Entities/MovieMetadata.cs @@ -28,7 +28,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// The title or name of the object /// ISO-639-3 3-character language codes @@ -65,7 +65,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Outline + /// Backing field for Outline. /// protected string _Outline; /// @@ -102,7 +102,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Plot + /// Backing field for Plot. /// protected string _Plot; /// @@ -139,7 +139,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Tagline + /// Backing field for Tagline. /// protected string _Tagline; /// @@ -176,7 +176,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Country + /// Backing field for Country. /// protected string _Country; /// diff --git a/Jellyfin.Data/Entities/MusicAlbum.cs b/Jellyfin.Data/Entities/MusicAlbum.cs index e07f4357b..9afea1fb6 100644 --- a/Jellyfin.Data/Entities/MusicAlbum.cs +++ b/Jellyfin.Data/Entities/MusicAlbum.cs @@ -28,7 +28,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// This is whats gets displayed in the Urls and API requests. This could also be a string. public MusicAlbum(Guid urlid, DateTime dateadded) diff --git a/Jellyfin.Data/Entities/MusicAlbumMetadata.cs b/Jellyfin.Data/Entities/MusicAlbumMetadata.cs index 7743890a6..0ba594654 100644 --- a/Jellyfin.Data/Entities/MusicAlbumMetadata.cs +++ b/Jellyfin.Data/Entities/MusicAlbumMetadata.cs @@ -28,7 +28,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// The title or name of the object /// ISO-639-3 3-character language codes @@ -65,7 +65,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Barcode + /// Backing field for Barcode. /// protected string _Barcode; /// @@ -102,7 +102,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for LabelNumber + /// Backing field for LabelNumber. /// protected string _LabelNumber; /// @@ -139,7 +139,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Country + /// Backing field for Country. /// protected string _Country; /// diff --git a/Jellyfin.Data/Entities/Person.cs b/Jellyfin.Data/Entities/Person.cs index f71418819..8f6fb3a7d 100644 --- a/Jellyfin.Data/Entities/Person.cs +++ b/Jellyfin.Data/Entities/Person.cs @@ -28,7 +28,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// /// @@ -59,7 +59,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Id + /// Backing field for Id. /// internal int _Id; /// @@ -72,7 +72,7 @@ namespace Jellyfin.Data.Entities partial void GetId(ref int result); /// - /// Identity, Indexed, Required + /// Identity, Indexed, Required. /// [Key] [Required] @@ -97,7 +97,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for UrlId + /// Backing field for UrlId. /// protected Guid _UrlId; /// @@ -110,7 +110,7 @@ namespace Jellyfin.Data.Entities partial void GetUrlId(ref Guid result); /// - /// Required + /// Required. /// [Required] public Guid UrlId @@ -133,7 +133,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Name + /// Backing field for Name. /// protected string _Name; /// @@ -171,7 +171,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for SourceId + /// Backing field for SourceId. /// protected string _SourceId; /// @@ -208,7 +208,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for DateAdded + /// Backing field for DateAdded. /// protected DateTime _DateAdded; /// @@ -221,7 +221,7 @@ namespace Jellyfin.Data.Entities partial void GetDateAdded(ref DateTime result); /// - /// Required + /// Required. /// [Required] public DateTime DateAdded @@ -244,7 +244,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for DateModified + /// Backing field for DateModified. /// protected DateTime _DateModified; /// @@ -257,7 +257,7 @@ namespace Jellyfin.Data.Entities partial void GetDateModified(ref DateTime result); /// - /// Required + /// Required. /// [Required] public DateTime DateModified @@ -280,7 +280,7 @@ namespace Jellyfin.Data.Entities } /// - /// Required, ConcurrenyToken + /// Required, ConcurrenyToken. /// [ConcurrencyCheck] [Required] diff --git a/Jellyfin.Data/Entities/PersonRole.cs b/Jellyfin.Data/Entities/PersonRole.cs index a3d047115..9bd477938 100644 --- a/Jellyfin.Data/Entities/PersonRole.cs +++ b/Jellyfin.Data/Entities/PersonRole.cs @@ -31,7 +31,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// /// @@ -65,7 +65,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Id + /// Backing field for Id. /// internal int _Id; /// @@ -78,7 +78,7 @@ namespace Jellyfin.Data.Entities partial void GetId(ref int result); /// - /// Identity, Indexed, Required + /// Identity, Indexed, Required. /// [Key] [Required] @@ -103,7 +103,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Role + /// Backing field for Role. /// protected string _Role; /// @@ -140,7 +140,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Type + /// Backing field for Type. /// protected Enums.PersonRoleType _Type; /// @@ -153,7 +153,7 @@ namespace Jellyfin.Data.Entities partial void GetType(ref Enums.PersonRoleType result); /// - /// Required + /// Required. /// [Required] public Enums.PersonRoleType Type @@ -176,7 +176,7 @@ namespace Jellyfin.Data.Entities } /// - /// Required, ConcurrenyToken + /// Required, ConcurrenyToken. /// [ConcurrencyCheck] [Required] @@ -192,7 +192,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Required + /// Required. /// [ForeignKey("Person_Id")] diff --git a/Jellyfin.Data/Entities/Photo.cs b/Jellyfin.Data/Entities/Photo.cs index 226730126..9da55fe43 100644 --- a/Jellyfin.Data/Entities/Photo.cs +++ b/Jellyfin.Data/Entities/Photo.cs @@ -28,7 +28,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// This is whats gets displayed in the Urls and API requests. This could also be a string. public Photo(Guid urlid, DateTime dateadded) diff --git a/Jellyfin.Data/Entities/PhotoMetadata.cs b/Jellyfin.Data/Entities/PhotoMetadata.cs index 2bb239cdd..5a9cf5b66 100644 --- a/Jellyfin.Data/Entities/PhotoMetadata.cs +++ b/Jellyfin.Data/Entities/PhotoMetadata.cs @@ -24,7 +24,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// The title or name of the object /// ISO-639-3 3-character language codes diff --git a/Jellyfin.Data/Entities/ProviderMapping.cs b/Jellyfin.Data/Entities/ProviderMapping.cs index e86d9737f..4125eabcd 100644 --- a/Jellyfin.Data/Entities/ProviderMapping.cs +++ b/Jellyfin.Data/Entities/ProviderMapping.cs @@ -25,7 +25,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// /// @@ -65,7 +65,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Identity, Indexed, Required + /// Identity, Indexed, Required. /// [Key] [Required] @@ -97,7 +97,7 @@ namespace Jellyfin.Data.Entities public string ProviderData { get; set; } /// - /// Required, ConcurrenyToken + /// Required, ConcurrenyToken. /// [ConcurrencyCheck] [Required] diff --git a/Jellyfin.Data/Entities/Rating.cs b/Jellyfin.Data/Entities/Rating.cs index 0c8b99ca2..34f395a22 100644 --- a/Jellyfin.Data/Entities/Rating.cs +++ b/Jellyfin.Data/Entities/Rating.cs @@ -25,7 +25,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// /// @@ -55,7 +55,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Id + /// Backing field for Id. /// internal int _Id; /// @@ -68,7 +68,7 @@ namespace Jellyfin.Data.Entities partial void GetId(ref int result); /// - /// Identity, Indexed, Required + /// Identity, Indexed, Required. /// [Key] [Required] @@ -93,7 +93,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Value + /// Backing field for Value. /// protected double _Value; /// @@ -106,7 +106,7 @@ namespace Jellyfin.Data.Entities partial void GetValue(ref double result); /// - /// Required + /// Required. /// [Required] public double Value @@ -129,7 +129,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Votes + /// Backing field for Votes. /// protected int? _Votes; /// @@ -161,7 +161,7 @@ namespace Jellyfin.Data.Entities } /// - /// Required, ConcurrenyToken + /// Required, ConcurrenyToken. /// [ConcurrencyCheck] [Required] diff --git a/Jellyfin.Data/Entities/RatingSource.cs b/Jellyfin.Data/Entities/RatingSource.cs index c829042b5..5ea6069f9 100644 --- a/Jellyfin.Data/Entities/RatingSource.cs +++ b/Jellyfin.Data/Entities/RatingSource.cs @@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations.Schema; namespace Jellyfin.Data.Entities { /// - /// This is the entity to store review ratings, not age ratings + /// This is the entity to store review ratings, not age ratings. /// public partial class RatingSource { @@ -28,7 +28,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// /// @@ -62,7 +62,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Id + /// Backing field for Id. /// internal int _Id; /// @@ -75,7 +75,7 @@ namespace Jellyfin.Data.Entities partial void GetId(ref int result); /// - /// Identity, Indexed, Required + /// Identity, Indexed, Required. /// [Key] [Required] @@ -100,7 +100,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Name + /// Backing field for Name. /// protected string _Name; /// @@ -137,7 +137,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for MaximumValue + /// Backing field for MaximumValue. /// protected double _MaximumValue; /// @@ -150,7 +150,7 @@ namespace Jellyfin.Data.Entities partial void GetMaximumValue(ref double result); /// - /// Required + /// Required. /// [Required] public double MaximumValue @@ -173,7 +173,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for MinimumValue + /// Backing field for MinimumValue. /// protected double _MinimumValue; /// @@ -186,7 +186,7 @@ namespace Jellyfin.Data.Entities partial void GetMinimumValue(ref double result); /// - /// Required + /// Required. /// [Required] public double MinimumValue @@ -209,7 +209,7 @@ namespace Jellyfin.Data.Entities } /// - /// Required, ConcurrenyToken + /// Required, ConcurrenyToken. /// [ConcurrencyCheck] [Required] diff --git a/Jellyfin.Data/Entities/Release.cs b/Jellyfin.Data/Entities/Release.cs index 35fcbb4b7..fe0dbaa54 100644 --- a/Jellyfin.Data/Entities/Release.cs +++ b/Jellyfin.Data/Entities/Release.cs @@ -29,7 +29,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// /// @@ -87,7 +87,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Id + /// Backing field for Id. /// internal int _Id; /// @@ -100,7 +100,7 @@ namespace Jellyfin.Data.Entities partial void GetId(ref int result); /// - /// Identity, Indexed, Required + /// Identity, Indexed, Required. /// [Key] [Required] @@ -125,7 +125,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Name + /// Backing field for Name. /// protected string _Name; /// @@ -163,7 +163,7 @@ namespace Jellyfin.Data.Entities } /// - /// Required, ConcurrenyToken + /// Required, ConcurrenyToken. /// [ConcurrencyCheck] [Required] diff --git a/Jellyfin.Data/Entities/Season.cs b/Jellyfin.Data/Entities/Season.cs index 2a861b660..81f413fa8 100644 --- a/Jellyfin.Data/Entities/Season.cs +++ b/Jellyfin.Data/Entities/Season.cs @@ -31,7 +31,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// This is whats gets displayed in the Urls and API requests. This could also be a string. /// @@ -66,7 +66,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for SeasonNumber + /// Backing field for SeasonNumber. /// protected int? _SeasonNumber; /// diff --git a/Jellyfin.Data/Entities/SeasonMetadata.cs b/Jellyfin.Data/Entities/SeasonMetadata.cs index 10320c6bb..b323984f1 100644 --- a/Jellyfin.Data/Entities/SeasonMetadata.cs +++ b/Jellyfin.Data/Entities/SeasonMetadata.cs @@ -25,7 +25,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// The title or name of the object /// ISO-639-3 3-character language codes @@ -61,7 +61,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Outline + /// Backing field for Outline. /// protected string _Outline; /// diff --git a/Jellyfin.Data/Entities/Series.cs b/Jellyfin.Data/Entities/Series.cs index cf1d6b781..013c8385f 100644 --- a/Jellyfin.Data/Entities/Series.cs +++ b/Jellyfin.Data/Entities/Series.cs @@ -20,7 +20,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// This is whats gets displayed in the Urls and API requests. This could also be a string. public Series(Guid urlid, DateTime dateadded) @@ -47,7 +47,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for AirsDayOfWeek + /// Backing field for AirsDayOfWeek. /// protected DayOfWeek? _AirsDayOfWeek; /// @@ -79,7 +79,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for AirsTime + /// Backing field for AirsTime. /// protected DateTimeOffset? _AirsTime; /// @@ -92,7 +92,7 @@ namespace Jellyfin.Data.Entities partial void GetAirsTime(ref DateTimeOffset? result); /// - /// The time the show airs, ignore the date portion + /// The time the show airs, ignore the date portion. /// public DateTimeOffset? AirsTime { @@ -114,7 +114,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for FirstAired + /// Backing field for FirstAired. /// protected DateTimeOffset? _FirstAired; /// diff --git a/Jellyfin.Data/Entities/SeriesMetadata.cs b/Jellyfin.Data/Entities/SeriesMetadata.cs index bb31c2e4e..4666d79ef 100644 --- a/Jellyfin.Data/Entities/SeriesMetadata.cs +++ b/Jellyfin.Data/Entities/SeriesMetadata.cs @@ -28,7 +28,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// The title or name of the object /// ISO-639-3 3-character language codes @@ -65,7 +65,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for Outline + /// Backing field for Outline. /// protected string _Outline; /// @@ -102,7 +102,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Plot + /// Backing field for Plot. /// protected string _Plot; /// @@ -139,7 +139,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Tagline + /// Backing field for Tagline. /// protected string _Tagline; /// @@ -176,7 +176,7 @@ namespace Jellyfin.Data.Entities } /// - /// Backing field for Country + /// Backing field for Country. /// protected string _Country; /// diff --git a/Jellyfin.Data/Entities/Track.cs b/Jellyfin.Data/Entities/Track.cs index c9e8fd1c3..1c3562a65 100644 --- a/Jellyfin.Data/Entities/Track.cs +++ b/Jellyfin.Data/Entities/Track.cs @@ -31,7 +31,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// This is whats gets displayed in the Urls and API requests. This could also be a string. /// @@ -66,7 +66,7 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Backing field for TrackNumber + /// Backing field for TrackNumber. /// protected int? _TrackNumber; /// diff --git a/Jellyfin.Data/Entities/TrackMetadata.cs b/Jellyfin.Data/Entities/TrackMetadata.cs index 7b99c0683..05bb953f8 100644 --- a/Jellyfin.Data/Entities/TrackMetadata.cs +++ b/Jellyfin.Data/Entities/TrackMetadata.cs @@ -24,7 +24,7 @@ namespace Jellyfin.Data.Entities } /// - /// Public constructor with required data + /// Public constructor with required data. /// /// The title or name of the object /// ISO-639-3 3-character language codes diff --git a/Jellyfin.Data/Enums/PreferenceKind.cs b/Jellyfin.Data/Enums/PreferenceKind.cs index de8eecc73..a54d789af 100644 --- a/Jellyfin.Data/Enums/PreferenceKind.cs +++ b/Jellyfin.Data/Enums/PreferenceKind.cs @@ -26,7 +26,7 @@ namespace Jellyfin.Data.Enums EnabledDevices = 3, /// - /// A list of enabled channels + /// A list of enabled channels. /// EnabledChannels = 4, diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index f574ebc66..a3fc6291f 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -69,7 +69,7 @@ namespace Jellyfin.Server.Implementations /// /// Repository for global::Jellyfin.Data.Entities.RatingSource - This is the entity to - /// store review ratings, not age ratings + /// store review ratings, not age ratings. /// public virtual DbSet RatingSources { get; set; } public virtual DbSet Releases { get; set; } diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index 9e651fb19..b041effb2 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -43,7 +43,7 @@ namespace MediaBrowser.Api private readonly IMediaSourceManager _mediaSourceManager; /// - /// The active transcoding jobs + /// The active transcoding jobs. /// private readonly List _activeTranscodingJobs = new List(); @@ -293,7 +293,7 @@ namespace MediaBrowser.Api /// /// - /// The progressive + /// The progressive. /// /// Called when [transcode failed to start]. /// diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs index a91a9b580..63a31a745 100644 --- a/MediaBrowser.Api/BaseApiService.cs +++ b/MediaBrowser.Api/BaseApiService.cs @@ -17,7 +17,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api { /// - /// Class BaseApiService + /// Class BaseApiService. /// public abstract class BaseApiService : IService, IRequiresRequest { diff --git a/MediaBrowser.Api/ChannelService.cs b/MediaBrowser.Api/ChannelService.cs index 3cab9fb66..8c336b1c9 100644 --- a/MediaBrowser.Api/ChannelService.cs +++ b/MediaBrowser.Api/ChannelService.cs @@ -36,7 +36,7 @@ namespace MediaBrowser.Api public int? StartIndex { get; set; } /// - /// The maximum number of items to return + /// The maximum number of items to return. /// /// The limit. [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] @@ -90,7 +90,7 @@ namespace MediaBrowser.Api public int? StartIndex { get; set; } /// - /// The maximum number of items to return + /// The maximum number of items to return. /// /// The limit. [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] @@ -149,7 +149,7 @@ namespace MediaBrowser.Api public int? StartIndex { get; set; } /// - /// The maximum number of items to return + /// The maximum number of items to return. /// /// The limit. [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] diff --git a/MediaBrowser.Api/ConfigurationService.cs b/MediaBrowser.Api/ConfigurationService.cs index 3ad51de8d..19369ccca 100644 --- a/MediaBrowser.Api/ConfigurationService.cs +++ b/MediaBrowser.Api/ConfigurationService.cs @@ -11,7 +11,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api { /// - /// Class GetConfiguration + /// Class GetConfiguration. /// [Route("/System/Configuration", "GET", Summary = "Gets application configuration")] [Authenticated] @@ -28,7 +28,7 @@ namespace MediaBrowser.Api } /// - /// Class UpdateConfiguration + /// Class UpdateConfiguration. /// [Route("/System/Configuration", "POST", Summary = "Updates application configuration")] [Authenticated(Roles = "Admin")] @@ -65,12 +65,12 @@ namespace MediaBrowser.Api public class ConfigurationService : BaseApiService { /// - /// The _json serializer + /// The _json serializer. /// private readonly IJsonSerializer _jsonSerializer; /// - /// The _configuration manager + /// The _configuration manager. /// private readonly IServerConfigurationManager _configurationManager; diff --git a/MediaBrowser.Api/DisplayPreferencesService.cs b/MediaBrowser.Api/DisplayPreferencesService.cs index 62c4ff43f..c3ed40ad3 100644 --- a/MediaBrowser.Api/DisplayPreferencesService.cs +++ b/MediaBrowser.Api/DisplayPreferencesService.cs @@ -10,7 +10,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api { /// - /// Class UpdateDisplayPreferences + /// Class UpdateDisplayPreferences. /// [Route("/DisplayPreferences/{DisplayPreferencesId}", "POST", Summary = "Updates a user's display preferences for an item")] public class UpdateDisplayPreferences : DisplayPreferences, IReturnVoid @@ -44,17 +44,17 @@ namespace MediaBrowser.Api } /// - /// Class DisplayPreferencesService + /// Class DisplayPreferencesService. /// [Authenticated] public class DisplayPreferencesService : BaseApiService { /// - /// The _display preferences manager + /// The _display preferences manager. /// private readonly IDisplayPreferencesRepository _displayPreferencesManager; /// - /// The _json serializer + /// The _json serializer. /// private readonly IJsonSerializer _jsonSerializer; diff --git a/MediaBrowser.Api/EnvironmentService.cs b/MediaBrowser.Api/EnvironmentService.cs index fddf78465..70acd069e 100644 --- a/MediaBrowser.Api/EnvironmentService.cs +++ b/MediaBrowser.Api/EnvironmentService.cs @@ -12,7 +12,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api { /// - /// Class GetDirectoryContents + /// Class GetDirectoryContents. /// [Route("/Environment/DirectoryContents", "GET", Summary = "Gets the contents of a given directory in the file system")] public class GetDirectoryContents : IReturn> @@ -66,7 +66,7 @@ namespace MediaBrowser.Api } /// - /// Class GetDrives + /// Class GetDrives. /// [Route("/Environment/Drives", "GET", Summary = "Gets available drives from the server's file system")] public class GetDrives : IReturn> @@ -74,7 +74,7 @@ namespace MediaBrowser.Api } /// - /// Class GetNetworkComputers + /// Class GetNetworkComputers. /// [Route("/Environment/NetworkDevices", "GET", Summary = "Gets a list of devices on the network")] public class GetNetworkDevices : IReturn> @@ -103,7 +103,7 @@ namespace MediaBrowser.Api } /// - /// Class EnvironmentService + /// Class EnvironmentService. /// [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)] public class EnvironmentService : BaseApiService @@ -112,7 +112,7 @@ namespace MediaBrowser.Api private const string UncSeparatorString = "\\"; /// - /// The _network manager + /// The _network manager. /// private readonly INetworkManager _networkManager; private readonly IFileSystem _fileSystem; @@ -220,7 +220,7 @@ namespace MediaBrowser.Api } /// - /// Gets the list that is returned when an empty path is supplied + /// Gets the list that is returned when an empty path is supplied. /// /// IEnumerable{FileSystemEntryInfo}. private IEnumerable GetDrives() diff --git a/MediaBrowser.Api/IHasItemFields.cs b/MediaBrowser.Api/IHasItemFields.cs index 6359de77d..ad4f1b489 100644 --- a/MediaBrowser.Api/IHasItemFields.cs +++ b/MediaBrowser.Api/IHasItemFields.cs @@ -5,7 +5,7 @@ using MediaBrowser.Model.Querying; namespace MediaBrowser.Api { /// - /// Interface IHasItemFields + /// Interface IHasItemFields. /// public interface IHasItemFields { diff --git a/MediaBrowser.Api/Images/ImageByNameService.cs b/MediaBrowser.Api/Images/ImageByNameService.cs index 45b7d0c10..2d405ac3d 100644 --- a/MediaBrowser.Api/Images/ImageByNameService.cs +++ b/MediaBrowser.Api/Images/ImageByNameService.cs @@ -16,7 +16,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.Images { /// - /// Class GetGeneralImage + /// Class GetGeneralImage. /// [Route("/Images/General/{Name}/{Type}", "GET", Summary = "Gets a general image by name")] public class GetGeneralImage @@ -33,7 +33,7 @@ namespace MediaBrowser.Api.Images } /// - /// Class GetRatingImage + /// Class GetRatingImage. /// [Route("/Images/Ratings/{Theme}/{Name}", "GET", Summary = "Gets a rating image by name")] public class GetRatingImage @@ -54,7 +54,7 @@ namespace MediaBrowser.Api.Images } /// - /// Class GetMediaInfoImage + /// Class GetMediaInfoImage. /// [Route("/Images/MediaInfo/{Theme}/{Name}", "GET", Summary = "Gets a media info image by name")] public class GetMediaInfoImage @@ -93,12 +93,12 @@ namespace MediaBrowser.Api.Images } /// - /// Class ImageByNameService + /// Class ImageByNameService. /// public class ImageByNameService : BaseApiService { /// - /// The _app paths + /// The _app paths. /// private readonly IServerApplicationPaths _appPaths; diff --git a/MediaBrowser.Api/Images/ImageRequest.cs b/MediaBrowser.Api/Images/ImageRequest.cs index 71ff09b63..0f3455548 100644 --- a/MediaBrowser.Api/Images/ImageRequest.cs +++ b/MediaBrowser.Api/Images/ImageRequest.cs @@ -4,30 +4,30 @@ using MediaBrowser.Model.Services; namespace MediaBrowser.Api.Images { /// - /// Class ImageRequest + /// Class ImageRequest. /// public class ImageRequest : DeleteImageRequest { /// - /// The max width + /// The max width. /// [ApiMember(Name = "MaxWidth", Description = "The maximum image width to return.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? MaxWidth { get; set; } /// - /// The max height + /// The max height. /// [ApiMember(Name = "MaxHeight", Description = "The maximum image height to return.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? MaxHeight { get; set; } /// - /// The width + /// The width. /// [ApiMember(Name = "Width", Description = "The fixed image width to return.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? Width { get; set; } /// - /// The height + /// The height. /// [ApiMember(Name = "Height", Description = "The fixed image height to return.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? Height { get; set; } @@ -79,7 +79,7 @@ namespace MediaBrowser.Api.Images } /// - /// Class DeleteImageRequest + /// Class DeleteImageRequest. /// public class DeleteImageRequest { diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index 6f2956c5d..8426a9a4f 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -58,7 +58,7 @@ namespace MediaBrowser.Api.Images } /// - /// Class UpdateItemImageIndex + /// Class UpdateItemImageIndex. /// [Route("/Items/{Id}/Images/{Type}/{Index}/Index", "POST", Summary = "Updates the index for an item image")] [Authenticated(Roles = "admin")] @@ -94,7 +94,7 @@ namespace MediaBrowser.Api.Images } /// - /// Class GetPersonImage + /// Class GetPersonImage. /// [Route("/Artists/{Name}/Images/{Type}", "GET")] [Route("/Artists/{Name}/Images/{Type}/{Index}", "GET")] @@ -131,7 +131,7 @@ namespace MediaBrowser.Api.Images } /// - /// Class GetUserImage + /// Class GetUserImage. /// [Route("/Users/{Id}/Images/{Type}", "GET")] [Route("/Users/{Id}/Images/{Type}/{Index}", "GET")] @@ -148,7 +148,7 @@ namespace MediaBrowser.Api.Images } /// - /// Class DeleteItemImage + /// Class DeleteItemImage. /// [Route("/Items/{Id}/Images/{Type}", "DELETE")] [Route("/Items/{Id}/Images/{Type}/{Index}", "DELETE")] @@ -164,7 +164,7 @@ namespace MediaBrowser.Api.Images } /// - /// Class DeleteUserImage + /// Class DeleteUserImage. /// [Route("/Users/{Id}/Images/{Type}", "DELETE")] [Route("/Users/{Id}/Images/{Type}/{Index}", "DELETE")] @@ -180,7 +180,7 @@ namespace MediaBrowser.Api.Images } /// - /// Class PostUserImage + /// Class PostUserImage. /// [Route("/Users/{Id}/Images/{Type}", "POST")] [Route("/Users/{Id}/Images/{Type}/{Index}", "POST")] @@ -195,14 +195,14 @@ namespace MediaBrowser.Api.Images public string Id { get; set; } /// - /// The raw Http Request Input Stream + /// The raw Http Request Input Stream. /// /// The request stream. public Stream RequestStream { get; set; } } /// - /// Class PostItemImage + /// Class PostItemImage. /// [Route("/Items/{Id}/Images/{Type}", "POST")] [Route("/Items/{Id}/Images/{Type}/{Index}", "POST")] @@ -217,14 +217,14 @@ namespace MediaBrowser.Api.Images public string Id { get; set; } /// - /// The raw Http Request Input Stream + /// The raw Http Request Input Stream. /// /// The request stream. public Stream RequestStream { get; set; } } /// - /// Class ImageService + /// Class ImageService. /// public class ImageService : BaseApiService { diff --git a/MediaBrowser.Api/Images/RemoteImageService.cs b/MediaBrowser.Api/Images/RemoteImageService.cs index 2633a5d3c..86464b4b9 100644 --- a/MediaBrowser.Api/Images/RemoteImageService.cs +++ b/MediaBrowser.Api/Images/RemoteImageService.cs @@ -33,7 +33,7 @@ namespace MediaBrowser.Api.Images public int? StartIndex { get; set; } /// - /// The maximum number of items to return + /// The maximum number of items to return. /// /// The limit. [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs index eb64abb4d..1ab820680 100644 --- a/MediaBrowser.Api/Library/LibraryService.cs +++ b/MediaBrowser.Api/Library/LibraryService.cs @@ -49,7 +49,7 @@ namespace MediaBrowser.Api.Library } /// - /// Class GetCriticReviews + /// Class GetCriticReviews. /// [Route("/Items/{Id}/CriticReviews", "GET", Summary = "Gets critic reviews for an item")] [Authenticated] @@ -70,7 +70,7 @@ namespace MediaBrowser.Api.Library public int? StartIndex { get; set; } /// - /// The maximum number of items to return + /// The maximum number of items to return. /// /// The limit. [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] @@ -78,7 +78,7 @@ namespace MediaBrowser.Api.Library } /// - /// Class GetThemeSongs + /// Class GetThemeSongs. /// [Route("/Items/{Id}/ThemeSongs", "GET", Summary = "Gets theme songs for an item")] [Authenticated] @@ -103,7 +103,7 @@ namespace MediaBrowser.Api.Library } /// - /// Class GetThemeVideos + /// Class GetThemeVideos. /// [Route("/Items/{Id}/ThemeVideos", "GET", Summary = "Gets theme videos for an item")] [Authenticated] @@ -128,7 +128,7 @@ namespace MediaBrowser.Api.Library } /// - /// Class GetThemeVideos + /// Class GetThemeVideos. /// [Route("/Items/{Id}/ThemeMedia", "GET", Summary = "Gets theme videos and songs for an item")] [Authenticated] @@ -205,7 +205,7 @@ namespace MediaBrowser.Api.Library } /// - /// Class GetPhyscialPaths + /// Class GetPhyscialPaths. /// [Route("/Library/PhysicalPaths", "GET", Summary = "Gets a list of physical paths from virtual folders")] [Authenticated(Roles = "Admin")] @@ -312,7 +312,7 @@ namespace MediaBrowser.Api.Library } /// - /// Class LibraryService + /// Class LibraryService. /// public class LibraryService : BaseApiService { diff --git a/MediaBrowser.Api/Library/LibraryStructureService.cs b/MediaBrowser.Api/Library/LibraryStructureService.cs index 1e300814f..b69550ed1 100644 --- a/MediaBrowser.Api/Library/LibraryStructureService.cs +++ b/MediaBrowser.Api/Library/LibraryStructureService.cs @@ -19,7 +19,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.Library { /// - /// Class GetDefaultVirtualFolders + /// Class GetDefaultVirtualFolders. /// [Route("/Library/VirtualFolders", "GET")] public class GetVirtualFolders : IReturn> @@ -166,18 +166,18 @@ namespace MediaBrowser.Api.Library } /// - /// Class LibraryStructureService + /// Class LibraryStructureService. /// [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)] public class LibraryStructureService : BaseApiService { /// - /// The _app paths + /// The _app paths. /// private readonly IServerApplicationPaths _appPaths; /// - /// The _library manager + /// The _library manager. /// private readonly ILibraryManager _libraryManager; private readonly ILibraryMonitor _libraryMonitor; diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index b00a5fec8..8e13744c4 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -31,7 +31,7 @@ using Microsoft.Net.Http.Headers; namespace MediaBrowser.Api.LiveTv { /// - /// This is insecure right now to avoid windows phone refactoring + /// This is insecure right now to avoid windows phone refactoring. /// [Route("/LiveTv/Info", "GET", Summary = "Gets available live tv services.")] [Authenticated] @@ -72,7 +72,7 @@ namespace MediaBrowser.Api.LiveTv public bool? IsSports { get; set; } /// - /// The maximum number of items to return + /// The maximum number of items to return. /// /// The limit. [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] @@ -100,7 +100,7 @@ namespace MediaBrowser.Api.LiveTv public string EnableImageTypes { get; set; } /// - /// Fields to return within the items, in addition to basic information + /// Fields to return within the items, in addition to basic information. /// /// The fields. [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] @@ -188,7 +188,7 @@ namespace MediaBrowser.Api.LiveTv public string EnableImageTypes { get; set; } /// - /// Fields to return within the items, in addition to basic information + /// Fields to return within the items, in addition to basic information. /// /// The fields. [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] @@ -250,7 +250,7 @@ namespace MediaBrowser.Api.LiveTv public string EnableImageTypes { get; set; } /// - /// Fields to return within the items, in addition to basic information + /// Fields to return within the items, in addition to basic information. /// /// The fields. [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] @@ -410,7 +410,7 @@ namespace MediaBrowser.Api.LiveTv public Guid LibrarySeriesId { get; set; } /// - /// Fields to return within the items, in addition to basic information + /// Fields to return within the items, in addition to basic information. /// /// The fields. [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] @@ -473,7 +473,7 @@ namespace MediaBrowser.Api.LiveTv public string GenreIds { get; set; } /// - /// Fields to return within the items, in addition to basic information + /// Fields to return within the items, in addition to basic information. /// /// The fields. [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] diff --git a/MediaBrowser.Api/LocalizationService.cs b/MediaBrowser.Api/LocalizationService.cs index 6a69d2656..d6b5f5195 100644 --- a/MediaBrowser.Api/LocalizationService.cs +++ b/MediaBrowser.Api/LocalizationService.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api { /// - /// Class GetCultures + /// Class GetCultures. /// [Route("/Localization/Cultures", "GET", Summary = "Gets known cultures")] public class GetCultures : IReturn @@ -16,7 +16,7 @@ namespace MediaBrowser.Api } /// - /// Class GetCountries + /// Class GetCountries. /// [Route("/Localization/Countries", "GET", Summary = "Gets known countries")] public class GetCountries : IReturn @@ -24,7 +24,7 @@ namespace MediaBrowser.Api } /// - /// Class ParentalRatings + /// Class ParentalRatings. /// [Route("/Localization/ParentalRatings", "GET", Summary = "Gets known parental ratings")] public class GetParentalRatings : IReturn @@ -32,7 +32,7 @@ namespace MediaBrowser.Api } /// - /// Class ParentalRatings + /// Class ParentalRatings. /// [Route("/Localization/Options", "GET", Summary = "Gets localization options")] public class GetLocalizationOptions : IReturn @@ -40,13 +40,13 @@ namespace MediaBrowser.Api } /// - /// Class CulturesService + /// Class CulturesService. /// [Authenticated(AllowBeforeStartupWizard = true)] public class LocalizationService : BaseApiService { /// - /// The _localization + /// The _localization. /// private readonly ILocalizationManager _localization; diff --git a/MediaBrowser.Api/Movies/MoviesService.cs b/MediaBrowser.Api/Movies/MoviesService.cs index 88ca0aa23..34cccffa3 100644 --- a/MediaBrowser.Api/Movies/MoviesService.cs +++ b/MediaBrowser.Api/Movies/MoviesService.cs @@ -65,13 +65,13 @@ namespace MediaBrowser.Api.Movies } /// - /// Class MoviesService + /// Class MoviesService. /// [Authenticated] public class MoviesService : BaseApiService { /// - /// The _user manager + /// The _user manager. /// private readonly IUserManager _userManager; diff --git a/MediaBrowser.Api/Movies/TrailersService.cs b/MediaBrowser.Api/Movies/TrailersService.cs index a7758b100..ca9f9d03b 100644 --- a/MediaBrowser.Api/Movies/TrailersService.cs +++ b/MediaBrowser.Api/Movies/TrailersService.cs @@ -18,18 +18,18 @@ namespace MediaBrowser.Api.Movies } /// - /// Class TrailersService + /// Class TrailersService. /// [Authenticated] public class TrailersService : BaseApiService { /// - /// The _user manager + /// The _user manager. /// private readonly IUserManager _userManager; /// - /// The _library manager + /// The _library manager. /// private readonly ILibraryManager _libraryManager; diff --git a/MediaBrowser.Api/Music/AlbumsService.cs b/MediaBrowser.Api/Music/AlbumsService.cs index f257d1014..74d3cce12 100644 --- a/MediaBrowser.Api/Music/AlbumsService.cs +++ b/MediaBrowser.Api/Music/AlbumsService.cs @@ -27,16 +27,16 @@ namespace MediaBrowser.Api.Music public class AlbumsService : BaseApiService { /// - /// The _user manager + /// The _user manager. /// private readonly IUserManager _userManager; /// - /// The _user data repository + /// The _user data repository. /// private readonly IUserDataManager _userDataRepository; /// - /// The _library manager + /// The _library manager. /// private readonly ILibraryManager _libraryManager; private readonly IItemRepository _itemRepo; diff --git a/MediaBrowser.Api/PackageService.cs b/MediaBrowser.Api/PackageService.cs index 444354a99..a63d06ad5 100644 --- a/MediaBrowser.Api/PackageService.cs +++ b/MediaBrowser.Api/PackageService.cs @@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api { /// - /// Class GetPackage + /// Class GetPackage. /// [Route("/Packages/{Name}", "GET", Summary = "Gets a package, by name or assembly guid")] [Authenticated] @@ -36,7 +36,7 @@ namespace MediaBrowser.Api } /// - /// Class GetPackages + /// Class GetPackages. /// [Route("/Packages", "GET", Summary = "Gets available packages")] [Authenticated] @@ -45,7 +45,7 @@ namespace MediaBrowser.Api } /// - /// Class InstallPackage + /// Class InstallPackage. /// [Route("/Packages/Installed/{Name}", "POST", Summary = "Installs a package")] [Authenticated(Roles = "Admin")] @@ -74,7 +74,7 @@ namespace MediaBrowser.Api } /// - /// Class CancelPackageInstallation + /// Class CancelPackageInstallation. /// [Route("/Packages/Installing/{Id}", "DELETE", Summary = "Cancels a package installation")] [Authenticated(Roles = "Admin")] @@ -89,7 +89,7 @@ namespace MediaBrowser.Api } /// - /// Class PackageService + /// Class PackageService. /// public class PackageService : BaseApiService { diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 2eb6198c0..afbafa9d3 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -28,7 +28,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.Playback { /// - /// Class BaseStreamingService + /// Class BaseStreamingService. /// public abstract class BaseStreamingService : BaseApiService { diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index c2d49a93b..f41f006fa 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -20,7 +20,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.Playback.Hls { /// - /// Class BaseHlsService + /// Class BaseHlsService. /// public abstract class BaseHlsService : BaseStreamingService { diff --git a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs index 87ccde2e0..8a3d00283 100644 --- a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs +++ b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs @@ -13,7 +13,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.Playback.Hls { /// - /// Class GetHlsAudioSegment + /// Class GetHlsAudioSegment. /// // Can't require authentication just yet due to seeing some requests come from Chrome without full query string //[Authenticated] @@ -37,7 +37,7 @@ namespace MediaBrowser.Api.Playback.Hls } /// - /// Class GetHlsVideoSegment + /// Class GetHlsVideoSegment. /// [Route("/Videos/{Id}/hls/{PlaylistId}/stream.m3u8", "GET")] [Authenticated] @@ -66,7 +66,7 @@ namespace MediaBrowser.Api.Playback.Hls } /// - /// Class GetHlsVideoSegment + /// Class GetHlsVideoSegment. /// // Can't require authentication just yet due to seeing some requests come from Chrome without full query string //[Authenticated] diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs index aefb3f019..9562f9953 100644 --- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs @@ -22,7 +22,7 @@ namespace MediaBrowser.Api.Playback.Hls } /// - /// Class VideoHlsService + /// Class VideoHlsService. /// [Authenticated] public class VideoHlsService : BaseHlsService diff --git a/MediaBrowser.Api/Playback/Progressive/AudioService.cs b/MediaBrowser.Api/Playback/Progressive/AudioService.cs index 34c7986ca..d51787df2 100644 --- a/MediaBrowser.Api/Playback/Progressive/AudioService.cs +++ b/MediaBrowser.Api/Playback/Progressive/AudioService.cs @@ -15,7 +15,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.Playback.Progressive { /// - /// Class GetAudioStream + /// Class GetAudioStream. /// [Route("/Audio/{Id}/stream.{Container}", "GET", Summary = "Gets an audio stream")] [Route("/Audio/{Id}/stream", "GET", Summary = "Gets an audio stream")] @@ -26,7 +26,7 @@ namespace MediaBrowser.Api.Playback.Progressive } /// - /// Class AudioService + /// Class AudioService. /// // TODO: In order to autheneticate this in the future, Dlna playback will require updating //[Authenticated] diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index 43cde440c..4820cbd92 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -21,7 +21,7 @@ using Microsoft.Net.Http.Headers; namespace MediaBrowser.Api.Playback.Progressive { /// - /// Class BaseProgressiveStreamingService + /// Class BaseProgressiveStreamingService. /// public abstract class BaseProgressiveStreamingService : BaseStreamingService { diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index a35e6c201..c3f6b905c 100644 --- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs +++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs @@ -15,7 +15,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.Playback.Progressive { /// - /// Class GetVideoStream + /// Class GetVideoStream. /// [Route("/Videos/{Id}/stream.mpegts", "GET")] [Route("/Videos/{Id}/stream.ts", "GET")] @@ -62,7 +62,7 @@ namespace MediaBrowser.Api.Playback.Progressive } /// - /// Class VideoService + /// Class VideoService. /// // TODO: In order to autheneticate this in the future, Dlna playback will require updating //[Authenticated] diff --git a/MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs b/MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs index 3b8b29995..7e2e337ad 100644 --- a/MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs +++ b/MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs @@ -8,17 +8,17 @@ using MediaBrowser.Model.Services; namespace MediaBrowser.Api.Playback { /// - /// Class StaticRemoteStreamWriter + /// Class StaticRemoteStreamWriter. /// public class StaticRemoteStreamWriter : IAsyncStreamWriter, IHasHeaders { /// - /// The _input stream + /// The _input stream. /// private readonly HttpResponseInfo _response; /// - /// The _options + /// The _options. /// private readonly IDictionary _options = new Dictionary(); diff --git a/MediaBrowser.Api/Playback/StreamRequest.cs b/MediaBrowser.Api/Playback/StreamRequest.cs index 9ba8eda91..cfcc98035 100644 --- a/MediaBrowser.Api/Playback/StreamRequest.cs +++ b/MediaBrowser.Api/Playback/StreamRequest.cs @@ -4,7 +4,7 @@ using MediaBrowser.Model.Services; namespace MediaBrowser.Api.Playback { /// - /// Class StreamRequest + /// Class StreamRequest. /// public class StreamRequest : BaseEncodingJobOptions { diff --git a/MediaBrowser.Api/PlaylistService.cs b/MediaBrowser.Api/PlaylistService.cs index d5def03be..5513c0892 100644 --- a/MediaBrowser.Api/PlaylistService.cs +++ b/MediaBrowser.Api/PlaylistService.cs @@ -95,14 +95,14 @@ namespace MediaBrowser.Api public int? StartIndex { get; set; } /// - /// The maximum number of items to return + /// The maximum number of items to return. /// /// The limit. [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? Limit { get; set; } /// - /// Fields to return within the items, in addition to basic information + /// Fields to return within the items, in addition to basic information. /// /// The fields. [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] diff --git a/MediaBrowser.Api/PluginService.cs b/MediaBrowser.Api/PluginService.cs index fd1075727..e9d5b2bc4 100644 --- a/MediaBrowser.Api/PluginService.cs +++ b/MediaBrowser.Api/PluginService.cs @@ -15,7 +15,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api { /// - /// Class Plugins + /// Class Plugins. /// [Route("/Plugins", "GET", Summary = "Gets a list of currently installed plugins")] [Authenticated] @@ -25,7 +25,7 @@ namespace MediaBrowser.Api } /// - /// Class UninstallPlugin + /// Class UninstallPlugin. /// [Route("/Plugins/{Id}", "DELETE", Summary = "Uninstalls a plugin")] [Authenticated(Roles = "Admin")] @@ -40,7 +40,7 @@ namespace MediaBrowser.Api } /// - /// Class GetPluginConfiguration + /// Class GetPluginConfiguration. /// [Route("/Plugins/{Id}/Configuration", "GET", Summary = "Gets a plugin's configuration")] [Authenticated] @@ -55,7 +55,7 @@ namespace MediaBrowser.Api } /// - /// Class UpdatePluginConfiguration + /// Class UpdatePluginConfiguration. /// [Route("/Plugins/{Id}/Configuration", "POST", Summary = "Updates a plugin's configuration")] [Authenticated] @@ -69,7 +69,7 @@ namespace MediaBrowser.Api public string Id { get; set; } /// - /// The raw Http Request Input Stream + /// The raw Http Request Input Stream. /// /// The request stream. public Stream RequestStream { get; set; } @@ -86,7 +86,7 @@ namespace MediaBrowser.Api } /// - /// Class GetPluginSecurityInfo + /// Class GetPluginSecurityInfo. /// [Route("/Plugins/SecurityInfo", "GET", Summary = "Gets plugin registration information", IsHidden = true)] [Authenticated] @@ -95,7 +95,7 @@ namespace MediaBrowser.Api } /// - /// Class UpdatePluginSecurityInfo + /// Class UpdatePluginSecurityInfo. /// [Route("/Plugins/SecurityInfo", "POST", Summary = "Updates plugin registration information", IsHidden = true)] [Authenticated(Roles = "Admin")] @@ -136,17 +136,17 @@ namespace MediaBrowser.Api public bool IsMBSupporter { get; set; } } /// - /// Class PluginsService + /// Class PluginsService. /// public class PluginService : BaseApiService { /// - /// The _json serializer + /// The _json serializer. /// private readonly IJsonSerializer _jsonSerializer; /// - /// The _app host + /// The _app host. /// private readonly IApplicationHost _appHost; private readonly IInstallationManager _installationManager; diff --git a/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs b/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs index e08a8482e..86b00316a 100644 --- a/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs +++ b/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs @@ -11,7 +11,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.ScheduledTasks { /// - /// Class GetScheduledTask + /// Class GetScheduledTask. /// [Route("/ScheduledTasks/{Id}", "GET", Summary = "Gets a scheduled task, by Id")] public class GetScheduledTask : IReturn @@ -25,7 +25,7 @@ namespace MediaBrowser.Api.ScheduledTasks } /// - /// Class GetScheduledTasks + /// Class GetScheduledTasks. /// [Route("/ScheduledTasks", "GET", Summary = "Gets scheduled tasks")] public class GetScheduledTasks : IReturn @@ -38,7 +38,7 @@ namespace MediaBrowser.Api.ScheduledTasks } /// - /// Class StartScheduledTask + /// Class StartScheduledTask. /// [Route("/ScheduledTasks/Running/{Id}", "POST", Summary = "Starts a scheduled task")] public class StartScheduledTask : IReturnVoid @@ -52,7 +52,7 @@ namespace MediaBrowser.Api.ScheduledTasks } /// - /// Class StopScheduledTask + /// Class StopScheduledTask. /// [Route("/ScheduledTasks/Running/{Id}", "DELETE", Summary = "Stops a scheduled task")] public class StopScheduledTask : IReturnVoid @@ -66,7 +66,7 @@ namespace MediaBrowser.Api.ScheduledTasks } /// - /// Class UpdateScheduledTaskTriggers + /// Class UpdateScheduledTaskTriggers. /// [Route("/ScheduledTasks/{Id}/Triggers", "POST", Summary = "Updates the triggers for a scheduled task")] public class UpdateScheduledTaskTriggers : List, IReturnVoid @@ -80,7 +80,7 @@ namespace MediaBrowser.Api.ScheduledTasks } /// - /// Class ScheduledTasksService + /// Class ScheduledTasksService. /// [Authenticated(Roles = "Admin")] public class ScheduledTaskService : BaseApiService diff --git a/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs b/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs index 14b9b3618..25dd39f2d 100644 --- a/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs +++ b/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs @@ -9,7 +9,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.ScheduledTasks { /// - /// Class ScheduledTasksWebSocketListener + /// Class ScheduledTasksWebSocketListener. /// public class ScheduledTasksWebSocketListener : BasePeriodicWebSocketListener, WebSocketListenerState> { diff --git a/MediaBrowser.Api/SearchService.cs b/MediaBrowser.Api/SearchService.cs index 4a2f96ed8..64ee69300 100644 --- a/MediaBrowser.Api/SearchService.cs +++ b/MediaBrowser.Api/SearchService.cs @@ -18,7 +18,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api { /// - /// Class GetSearchHints + /// Class GetSearchHints. /// [Route("/Search/Hints", "GET", Summary = "Gets search hints based on a search term")] public class GetSearchHints : IReturn @@ -31,7 +31,7 @@ namespace MediaBrowser.Api public int? StartIndex { get; set; } /// - /// The maximum number of items to return + /// The maximum number of items to return. /// /// The limit. [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] @@ -45,7 +45,7 @@ namespace MediaBrowser.Api public Guid UserId { get; set; } /// - /// Search characters used to find items + /// Search characters used to find items. /// /// The index by. [ApiMember(Name = "SearchTerm", Description = "The search term to filter on", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] @@ -104,13 +104,13 @@ namespace MediaBrowser.Api } /// - /// Class SearchService + /// Class SearchService. /// [Authenticated] public class SearchService : BaseApiService { /// - /// The _search engine + /// The _search engine. /// private readonly ISearchEngine _searchEngine; private readonly ILibraryManager _libraryManager; diff --git a/MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs b/MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs index 175984575..2400d6def 100644 --- a/MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs +++ b/MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.Sessions { /// - /// Class SessionInfoWebSocketListener + /// Class SessionInfoWebSocketListener. /// public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener, WebSocketListenerState> { @@ -19,7 +19,7 @@ namespace MediaBrowser.Api.Sessions protected override string Name => "Sessions"; /// - /// The _kernel + /// The _kernel. /// private readonly ISessionManager _sessionManager; diff --git a/MediaBrowser.Api/Sessions/SessionService.cs b/MediaBrowser.Api/Sessions/SessionService.cs index d986eea65..50adc5698 100644 --- a/MediaBrowser.Api/Sessions/SessionService.cs +++ b/MediaBrowser.Api/Sessions/SessionService.cs @@ -46,14 +46,14 @@ namespace MediaBrowser.Api.Sessions public string Id { get; set; } /// - /// Artist, Genre, Studio, Person, or any kind of BaseItem + /// Artist, Genre, Studio, Person, or any kind of BaseItem. /// /// The type of the item. [ApiMember(Name = "ItemType", Description = "The type of item to browse to.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] public string ItemType { get; set; } /// - /// Artist name, genre name, item Id, etc + /// Artist name, genre name, item Id, etc. /// /// The item identifier. [ApiMember(Name = "ItemId", Description = "The Id of the item.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] diff --git a/MediaBrowser.Api/SimilarItemsHelper.cs b/MediaBrowser.Api/SimilarItemsHelper.cs index 90c324ff3..7ed70f5d7 100644 --- a/MediaBrowser.Api/SimilarItemsHelper.cs +++ b/MediaBrowser.Api/SimilarItemsHelper.cs @@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api { /// - /// Class BaseGetSimilarItemsFromItem + /// Class BaseGetSimilarItemsFromItem. /// public class BaseGetSimilarItemsFromItem : BaseGetSimilarItems { @@ -50,14 +50,14 @@ namespace MediaBrowser.Api public Guid UserId { get; set; } /// - /// The maximum number of items to return + /// The maximum number of items to return. /// /// The limit. [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? Limit { get; set; } /// - /// Fields to return within the items, in addition to basic information + /// Fields to return within the items, in addition to basic information. /// /// The fields. [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] @@ -65,7 +65,7 @@ namespace MediaBrowser.Api } /// - /// Class SimilarItemsHelper + /// Class SimilarItemsHelper. /// public static class SimilarItemsHelper { diff --git a/MediaBrowser.Api/System/ActivityLogService.cs b/MediaBrowser.Api/System/ActivityLogService.cs index a6bacad4f..7ca31c21a 100644 --- a/MediaBrowser.Api/System/ActivityLogService.cs +++ b/MediaBrowser.Api/System/ActivityLogService.cs @@ -22,7 +22,7 @@ namespace MediaBrowser.Api.System public int? StartIndex { get; set; } /// - /// The maximum number of items to return + /// The maximum number of items to return. /// /// The limit. [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] diff --git a/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs b/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs index 8e4860be4..39976371a 100644 --- a/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs +++ b/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.System { /// - /// Class SessionInfoWebSocketListener + /// Class SessionInfoWebSocketListener. /// public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener { @@ -19,7 +19,7 @@ namespace MediaBrowser.Api.System protected override string Name => "ActivityLogEntry"; /// - /// The _kernel + /// The _kernel. /// private readonly IActivityManager _activityManager; diff --git a/MediaBrowser.Api/System/SystemService.cs b/MediaBrowser.Api/System/SystemService.cs index 4f10a4ad2..e0e20d828 100644 --- a/MediaBrowser.Api/System/SystemService.cs +++ b/MediaBrowser.Api/System/SystemService.cs @@ -18,7 +18,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.System { /// - /// Class GetSystemInfo + /// Class GetSystemInfo. /// [Route("/System/Info", "GET", Summary = "Gets information about the server")] [Authenticated(EscapeParentalControl = true, AllowBeforeStartupWizard = true)] @@ -38,7 +38,7 @@ namespace MediaBrowser.Api.System } /// - /// Class RestartApplication + /// Class RestartApplication. /// [Route("/System/Restart", "POST", Summary = "Restarts the application, if needed")] [Authenticated(Roles = "Admin", AllowLocal = true)] @@ -83,12 +83,12 @@ namespace MediaBrowser.Api.System } /// - /// Class SystemInfoService + /// Class SystemInfoService. /// public class SystemService : BaseApiService { /// - /// The _app host + /// The _app host. /// private readonly IServerApplicationHost _appHost; private readonly IApplicationPaths _appPaths; diff --git a/MediaBrowser.Api/TvShowsService.cs b/MediaBrowser.Api/TvShowsService.cs index 23062b67b..6177c2e2e 100644 --- a/MediaBrowser.Api/TvShowsService.cs +++ b/MediaBrowser.Api/TvShowsService.cs @@ -19,7 +19,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api { /// - /// Class GetNextUpEpisodes + /// Class GetNextUpEpisodes. /// [Route("/Shows/NextUp", "GET", Summary = "Gets a list of next up episodes")] public class GetNextUpEpisodes : IReturn>, IHasDtoOptions @@ -39,14 +39,14 @@ namespace MediaBrowser.Api public int? StartIndex { get; set; } /// - /// The maximum number of items to return + /// The maximum number of items to return. /// /// The limit. [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? Limit { get; set; } /// - /// Fields to return within the items, in addition to basic information + /// Fields to return within the items, in addition to basic information. /// /// The fields. [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] @@ -99,14 +99,14 @@ namespace MediaBrowser.Api public int? StartIndex { get; set; } /// - /// The maximum number of items to return + /// The maximum number of items to return. /// /// The limit. [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? Limit { get; set; } /// - /// Fields to return within the items, in addition to basic information + /// Fields to return within the items, in addition to basic information. /// /// The fields. [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] @@ -143,7 +143,7 @@ namespace MediaBrowser.Api public Guid UserId { get; set; } /// - /// Fields to return within the items, in addition to basic information + /// Fields to return within the items, in addition to basic information. /// /// The fields. [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] @@ -175,7 +175,7 @@ namespace MediaBrowser.Api public int? StartIndex { get; set; } /// - /// The maximum number of items to return + /// The maximum number of items to return. /// /// The limit. [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] @@ -211,7 +211,7 @@ namespace MediaBrowser.Api public Guid UserId { get; set; } /// - /// Fields to return within the items, in addition to basic information + /// Fields to return within the items, in addition to basic information. /// /// The fields. [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] @@ -243,18 +243,18 @@ namespace MediaBrowser.Api } /// - /// Class TvShowsService + /// Class TvShowsService. /// [Authenticated] public class TvShowsService : BaseApiService { /// - /// The _user manager + /// The _user manager. /// private readonly IUserManager _userManager; /// - /// The _library manager + /// The _library manager. /// private readonly ILibraryManager _libraryManager; diff --git a/MediaBrowser.Api/UserLibrary/ArtistsService.cs b/MediaBrowser.Api/UserLibrary/ArtistsService.cs index bef91d54d..9875e0208 100644 --- a/MediaBrowser.Api/UserLibrary/ArtistsService.cs +++ b/MediaBrowser.Api/UserLibrary/ArtistsService.cs @@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.UserLibrary { /// - /// Class GetArtists + /// Class GetArtists. /// [Route("/Artists", "GET", Summary = "Gets all artists from a given item, folder, or the entire library")] public class GetArtists : GetItemsByName @@ -45,7 +45,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class ArtistsService + /// Class ArtistsService. /// [Authenticated] public class ArtistsService : BaseItemsByNameService diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs index 4802849f4..a68c6fb6d 100644 --- a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs +++ b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs @@ -15,7 +15,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.UserLibrary { /// - /// Class BaseItemsByNameService + /// Class BaseItemsByNameService. /// /// The type of the T item type. public abstract class BaseItemsByNameService : BaseApiService @@ -52,7 +52,7 @@ namespace MediaBrowser.Api.UserLibrary protected IUserManager UserManager { get; } /// - /// Gets the library manager + /// Gets the library manager. /// protected ILibraryManager LibraryManager { get; } @@ -375,7 +375,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class GetItemsByName + /// Class GetItemsByName. /// public class GetItemsByName : BaseItemsRequest, IReturn> { diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs index 7561b5c89..5a4394425 100644 --- a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs +++ b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs @@ -111,14 +111,14 @@ namespace MediaBrowser.Api.UserLibrary public int? StartIndex { get; set; } /// - /// The maximum number of items to return + /// The maximum number of items to return. /// /// The limit. [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? Limit { get; set; } /// - /// Whether or not to perform the query recursively + /// Whether or not to perform the query recursively. /// /// true if recursive; otherwise, false. [ApiMember(Name = "Recursive", Description = "When searching within folders, this determines whether or not the search will be recursive. true/false", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] @@ -141,7 +141,7 @@ namespace MediaBrowser.Api.UserLibrary public string ParentId { get; set; } /// - /// Fields to return within the items, in addition to basic information + /// Fields to return within the items, in addition to basic information. /// /// The fields. [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] @@ -162,14 +162,14 @@ namespace MediaBrowser.Api.UserLibrary public string IncludeItemTypes { get; set; } /// - /// Filters to apply to the results + /// Filters to apply to the results. /// /// The filters. [ApiMember(Name = "Filters", Description = "Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] public string Filters { get; set; } /// - /// Gets or sets the Isfavorite option + /// Gets or sets the Isfavorite option. /// /// IsFavorite [ApiMember(Name = "IsFavorite", Description = "Optional filter by items that are marked as favorite, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] @@ -190,7 +190,7 @@ namespace MediaBrowser.Api.UserLibrary public string ImageTypes { get; set; } /// - /// What to sort the results by + /// What to sort the results by. /// /// The sort by. [ApiMember(Name = "SortBy", Description = "Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] @@ -200,7 +200,7 @@ namespace MediaBrowser.Api.UserLibrary public bool? IsPlayed { get; set; } /// - /// Limit results to items containing specific genres + /// Limit results to items containing specific genres. /// /// The genres. [ApiMember(Name = "Genres", Description = "Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] @@ -215,7 +215,7 @@ namespace MediaBrowser.Api.UserLibrary public string Tags { get; set; } /// - /// Limit results to items containing specific years + /// Limit results to items containing specific years. /// /// The years. [ApiMember(Name = "Years", Description = "Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] @@ -234,7 +234,7 @@ namespace MediaBrowser.Api.UserLibrary public string EnableImageTypes { get; set; } /// - /// Limit results to items containing a specific person + /// Limit results to items containing a specific person. /// /// The person. [ApiMember(Name = "Person", Description = "Optional. If specified, results will be filtered to include only those containing the specified person.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] @@ -244,14 +244,14 @@ namespace MediaBrowser.Api.UserLibrary public string PersonIds { get; set; } /// - /// If the Person filter is used, this can also be used to restrict to a specific person type + /// If the Person filter is used, this can also be used to restrict to a specific person type. /// /// The type of the person. [ApiMember(Name = "PersonTypes", Description = "Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string PersonTypes { get; set; } /// - /// Limit results to items containing specific studios + /// Limit results to items containing specific studios. /// /// The studios. [ApiMember(Name = "Studios", Description = "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] diff --git a/MediaBrowser.Api/UserLibrary/GenresService.cs b/MediaBrowser.Api/UserLibrary/GenresService.cs index 1fa272a5f..7bdfbac98 100644 --- a/MediaBrowser.Api/UserLibrary/GenresService.cs +++ b/MediaBrowser.Api/UserLibrary/GenresService.cs @@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.UserLibrary { /// - /// Class GetGenres + /// Class GetGenres. /// [Route("/Genres", "GET", Summary = "Gets all genres from a given item, folder, or the entire library")] public class GetGenres : GetItemsByName @@ -22,7 +22,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class GetGenre + /// Class GetGenre. /// [Route("/Genres/{Name}", "GET", Summary = "Gets a genre, by name")] public class GetGenre : IReturn @@ -43,7 +43,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class GenresService + /// Class GenresService. /// [Authenticated] public class GenresService : BaseItemsByNameService diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index 49d534c36..7efe0552c 100644 --- a/MediaBrowser.Api/UserLibrary/ItemsService.cs +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -20,7 +20,7 @@ using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; namespace MediaBrowser.Api.UserLibrary { /// - /// Class GetItems + /// Class GetItems. /// [Route("/Items", "GET", Summary = "Gets items based on a query.")] [Route("/Users/{UserId}/Items", "GET", Summary = "Gets items based on a query.")] @@ -34,18 +34,18 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class ItemsService + /// Class ItemsService. /// [Authenticated] public class ItemsService : BaseApiService { /// - /// The _user manager + /// The _user manager. /// private readonly IUserManager _userManager; /// - /// The _library manager + /// The _library manager. /// private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; @@ -496,7 +496,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class DateCreatedComparer + /// Class DateCreatedComparer. /// public class DateCreatedComparer : IComparer { diff --git a/MediaBrowser.Api/UserLibrary/PersonsService.cs b/MediaBrowser.Api/UserLibrary/PersonsService.cs index 3204e5219..7924339ed 100644 --- a/MediaBrowser.Api/UserLibrary/PersonsService.cs +++ b/MediaBrowser.Api/UserLibrary/PersonsService.cs @@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.UserLibrary { /// - /// Class GetPersons + /// Class GetPersons. /// [Route("/Persons", "GET", Summary = "Gets all persons from a given item, folder, or the entire library")] public class GetPersons : GetItemsByName @@ -22,7 +22,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class GetPerson + /// Class GetPerson. /// [Route("/Persons/{Name}", "GET", Summary = "Gets a person, by name")] public class GetPerson : IReturn @@ -43,7 +43,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class PersonsService + /// Class PersonsService. /// [Authenticated] public class PersonsService : BaseItemsByNameService diff --git a/MediaBrowser.Api/UserLibrary/PlaystateService.cs b/MediaBrowser.Api/UserLibrary/PlaystateService.cs index ab231626b..d809cc2e7 100644 --- a/MediaBrowser.Api/UserLibrary/PlaystateService.cs +++ b/MediaBrowser.Api/UserLibrary/PlaystateService.cs @@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.UserLibrary { /// - /// Class MarkPlayedItem + /// Class MarkPlayedItem. /// [Route("/Users/{UserId}/PlayedItems/{Id}", "POST", Summary = "Marks an item as played")] public class MarkPlayedItem : IReturn @@ -38,7 +38,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class MarkUnplayedItem + /// Class MarkUnplayedItem. /// [Route("/Users/{UserId}/PlayedItems/{Id}", "DELETE", Summary = "Marks an item as unplayed")] public class MarkUnplayedItem : IReturn @@ -81,7 +81,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class OnPlaybackStart + /// Class OnPlaybackStart. /// [Route("/Users/{UserId}/PlayingItems/{Id}", "POST", Summary = "Reports that a user has begun playing an item")] public class OnPlaybackStart : IReturnVoid @@ -123,7 +123,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class OnPlaybackProgress + /// Class OnPlaybackProgress. /// [Route("/Users/{UserId}/PlayingItems/{Id}/Progress", "POST", Summary = "Reports a user's playback progress")] public class OnPlaybackProgress : IReturnVoid @@ -181,7 +181,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class OnPlaybackStopped + /// Class OnPlaybackStopped. /// [Route("/Users/{UserId}/PlayingItems/{Id}", "DELETE", Summary = "Reports that a user has stopped playing an item")] public class OnPlaybackStopped : IReturnVoid diff --git a/MediaBrowser.Api/UserLibrary/StudiosService.cs b/MediaBrowser.Api/UserLibrary/StudiosService.cs index 683ce5d09..66350955f 100644 --- a/MediaBrowser.Api/UserLibrary/StudiosService.cs +++ b/MediaBrowser.Api/UserLibrary/StudiosService.cs @@ -13,7 +13,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.UserLibrary { /// - /// Class GetStudios + /// Class GetStudios. /// [Route("/Studios", "GET", Summary = "Gets all studios from a given item, folder, or the entire library")] public class GetStudios : GetItemsByName @@ -21,7 +21,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class GetStudio + /// Class GetStudio. /// [Route("/Studios/{Name}", "GET", Summary = "Gets a studio, by name")] public class GetStudio : IReturn @@ -42,7 +42,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class StudiosService + /// Class StudiosService. /// [Authenticated] public class StudiosService : BaseItemsByNameService diff --git a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs index f75852885..f9cbba410 100644 --- a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs +++ b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs @@ -20,7 +20,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.UserLibrary { /// - /// Class GetItem + /// Class GetItem. /// [Route("/Users/{UserId}/Items/{Id}", "GET", Summary = "Gets an item from a user's library")] public class GetItem : IReturn @@ -41,7 +41,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class GetItem + /// Class GetItem. /// [Route("/Users/{UserId}/Items/Root", "GET", Summary = "Gets the root folder from a user's library")] public class GetRootFolder : IReturn @@ -55,7 +55,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class GetIntros + /// Class GetIntros. /// [Route("/Users/{UserId}/Items/{Id}/Intros", "GET", Summary = "Gets intros to play before the main media item plays")] public class GetIntros : IReturn> @@ -76,7 +76,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class MarkFavoriteItem + /// Class MarkFavoriteItem. /// [Route("/Users/{UserId}/FavoriteItems/{Id}", "POST", Summary = "Marks an item as a favorite")] public class MarkFavoriteItem : IReturn @@ -97,7 +97,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class UnmarkFavoriteItem + /// Class UnmarkFavoriteItem. /// [Route("/Users/{UserId}/FavoriteItems/{Id}", "DELETE", Summary = "Unmarks an item as a favorite")] public class UnmarkFavoriteItem : IReturn @@ -118,7 +118,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class ClearUserItemRating + /// Class ClearUserItemRating. /// [Route("/Users/{UserId}/Items/{Id}/Rating", "DELETE", Summary = "Deletes a user's saved personal rating for an item")] public class DeleteUserItemRating : IReturn @@ -139,7 +139,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class UpdateUserItemRating + /// Class UpdateUserItemRating. /// [Route("/Users/{UserId}/Items/{Id}/Rating", "POST", Summary = "Updates a user's rating for an item")] public class UpdateUserItemRating : IReturn @@ -167,7 +167,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class GetLocalTrailers + /// Class GetLocalTrailers. /// [Route("/Users/{UserId}/Items/{Id}/LocalTrailers", "GET", Summary = "Gets local trailers for an item")] public class GetLocalTrailers : IReturn @@ -188,7 +188,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class GetSpecialFeatures + /// Class GetSpecialFeatures. /// [Route("/Users/{UserId}/Items/{Id}/SpecialFeatures", "GET", Summary = "Gets special features for an item")] public class GetSpecialFeatures : IReturn @@ -259,7 +259,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class UserLibraryService + /// Class UserLibraryService. /// [Authenticated] public class UserLibraryService : BaseApiService diff --git a/MediaBrowser.Api/UserLibrary/YearsService.cs b/MediaBrowser.Api/UserLibrary/YearsService.cs index d023ee90a..0523f89fa 100644 --- a/MediaBrowser.Api/UserLibrary/YearsService.cs +++ b/MediaBrowser.Api/UserLibrary/YearsService.cs @@ -13,7 +13,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.UserLibrary { /// - /// Class GetYears + /// Class GetYears. /// [Route("/Years", "GET", Summary = "Gets all years from a given item, folder, or the entire library")] public class GetYears : GetItemsByName @@ -21,7 +21,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class GetYear + /// Class GetYear. /// [Route("/Years/{Year}", "GET", Summary = "Gets a year")] public class GetYear : IReturn @@ -42,7 +42,7 @@ namespace MediaBrowser.Api.UserLibrary } /// - /// Class YearsService + /// Class YearsService. /// [Authenticated] public class YearsService : BaseItemsByNameService diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs index 9cb9baf63..131def554 100644 --- a/MediaBrowser.Api/UserService.cs +++ b/MediaBrowser.Api/UserService.cs @@ -19,7 +19,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api { /// - /// Class GetUsers + /// Class GetUsers. /// [Route("/Users", "GET", Summary = "Gets a list of users")] [Authenticated] @@ -41,7 +41,7 @@ namespace MediaBrowser.Api } /// - /// Class GetUser + /// Class GetUser. /// [Route("/Users/{Id}", "GET", Summary = "Gets a user by Id")] [Authenticated(EscapeParentalControl = true)] @@ -56,7 +56,7 @@ namespace MediaBrowser.Api } /// - /// Class DeleteUser + /// Class DeleteUser. /// [Route("/Users/{Id}", "DELETE", Summary = "Deletes a user")] [Authenticated(Roles = "Admin")] @@ -71,7 +71,7 @@ namespace MediaBrowser.Api } /// - /// Class AuthenticateUser + /// Class AuthenticateUser. /// [Route("/Users/{Id}/Authenticate", "POST", Summary = "Authenticates a user")] public class AuthenticateUser : IReturn @@ -95,7 +95,7 @@ namespace MediaBrowser.Api } /// - /// Class AuthenticateUser + /// Class AuthenticateUser. /// [Route("/Users/AuthenticateByName", "POST", Summary = "Authenticates a user")] public class AuthenticateUserByName : IReturn @@ -119,7 +119,7 @@ namespace MediaBrowser.Api } /// - /// Class UpdateUserPassword + /// Class UpdateUserPassword. /// [Route("/Users/{Id}/Password", "POST", Summary = "Updates a user's password")] [Authenticated] @@ -149,7 +149,7 @@ namespace MediaBrowser.Api } /// - /// Class UpdateUserEasyPassword + /// Class UpdateUserEasyPassword. /// [Route("/Users/{Id}/EasyPassword", "POST", Summary = "Updates a user's easy password")] [Authenticated] @@ -177,7 +177,7 @@ namespace MediaBrowser.Api } /// - /// Class UpdateUser + /// Class UpdateUser. /// [Route("/Users/{Id}", "POST", Summary = "Updates a user")] [Authenticated] @@ -186,7 +186,7 @@ namespace MediaBrowser.Api } /// - /// Class UpdateUser + /// Class UpdateUser. /// [Route("/Users/{Id}/Policy", "POST", Summary = "Updates a user policy")] [Authenticated(Roles = "admin")] @@ -197,7 +197,7 @@ namespace MediaBrowser.Api } /// - /// Class UpdateUser + /// Class UpdateUser. /// [Route("/Users/{Id}/Configuration", "POST", Summary = "Updates a user configuration")] [Authenticated] @@ -208,7 +208,7 @@ namespace MediaBrowser.Api } /// - /// Class CreateUser + /// Class CreateUser. /// [Route("/Users/New", "POST", Summary = "Creates a user")] [Authenticated(Roles = "Admin")] @@ -236,7 +236,7 @@ namespace MediaBrowser.Api } /// - /// Class UsersService + /// Class UsersService. /// public class UserService : BaseApiService { diff --git a/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs index 60455e68a..1f4a26064 100644 --- a/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs +++ b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs @@ -18,7 +18,7 @@ namespace MediaBrowser.Controller.Channels public List ContentTypes { get; set; } /// - /// Represents the maximum number of records the channel allows retrieving at a time + /// Represents the maximum number of records the channel allows retrieving at a time. /// public int? MaxPageSize { get; set; } diff --git a/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs b/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs index 6660743e6..a5c5e3bcc 100644 --- a/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs +++ b/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs @@ -4,7 +4,7 @@ using MediaBrowser.Model.Configuration; namespace MediaBrowser.Controller.Configuration { /// - /// Interface IServerConfigurationManager + /// Interface IServerConfigurationManager. /// public interface IServerConfigurationManager : IConfigurationManager { diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index f1873d539..488692c03 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -10,7 +10,7 @@ using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Drawing { /// - /// Interface IImageProcessor + /// Interface IImageProcessor. /// public interface IImageProcessor { diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs index 56e6c47c4..0dadc283e 100644 --- a/MediaBrowser.Controller/Dto/IDtoService.cs +++ b/MediaBrowser.Controller/Dto/IDtoService.cs @@ -7,7 +7,7 @@ using MediaBrowser.Model.Querying; namespace MediaBrowser.Controller.Dto { /// - /// Interface IDtoService + /// Interface IDtoService. /// public interface IDtoService { diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs index 54540e892..67858b844 100644 --- a/MediaBrowser.Controller/Entities/AggregateFolder.cs +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -35,7 +35,7 @@ namespace MediaBrowser.Controller.Entities public override bool SupportsPlayedStatus => false; /// - /// The _virtual children + /// The _virtual children. /// private readonly ConcurrentBag _virtualChildren = new ConcurrentBag(); diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index a8ea2157d..b3e241a15 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -11,7 +11,7 @@ using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Entities.Audio { /// - /// Class Audio + /// Class Audio. /// public class Audio : BaseItem, IHasAlbumArtist, diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index f7b2f9549..e563f398b 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -15,7 +15,7 @@ using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider; namespace MediaBrowser.Controller.Entities.Audio { /// - /// Class MusicAlbum + /// Class MusicAlbum. /// public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo, IMetadataContainer { diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 63db3cfab..56c3c38dc 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -15,7 +15,7 @@ using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider; namespace MediaBrowser.Controller.Entities.Audio { /// - /// Class MusicArtist + /// Class MusicArtist. /// public class MusicArtist : Folder, IItemByName, IHasMusicGenres, IHasDualAccess, IHasLookupInfo { @@ -111,7 +111,7 @@ namespace MediaBrowser.Controller.Entities.Audio /// /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself + /// If the item is a folder, it returns the folder itself. /// /// The containing folder path. [JsonIgnore] @@ -201,7 +201,7 @@ namespace MediaBrowser.Controller.Entities.Audio } /// - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true or false indicating if changes were made. /// public override bool BeforeMetadataRefresh(bool replaceAllMetdata) { diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs index 537e9630b..49a4cbae2 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities.Audio { /// - /// Class MusicGenre + /// Class MusicGenre. /// public class MusicGenre : BaseItem, IItemByName { @@ -34,7 +34,7 @@ namespace MediaBrowser.Controller.Entities.Audio /// /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself + /// If the item is a folder, it returns the folder itself. /// /// The containing folder path. [JsonIgnore] @@ -98,7 +98,7 @@ namespace MediaBrowser.Controller.Entities.Audio } /// - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true or false indicating if changes were made. /// public override bool BeforeMetadataRefresh(bool replaceAllMetdata) { diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index f2de1f2b1..ecbb83d07 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -31,12 +31,12 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities { /// - /// Class BaseItem + /// Class BaseItem. /// public abstract class BaseItem : IHasProviderIds, IHasLookupInfo, IEquatable { /// - /// The supported image extensions + /// The supported image extensions. /// public static readonly string[] SupportedImageExtensions = new[] { ".png", ".jpg", ".jpeg", ".tbn", ".gif" }; @@ -75,7 +75,7 @@ namespace MediaBrowser.Controller.Entities public static char SlugChar = '-'; /// - /// The trailer folder name + /// The trailer folder name. /// public const string TrailerFolderName = "trailers"; public const string ThemeSongsFolderName = "theme-music"; @@ -243,7 +243,7 @@ namespace MediaBrowser.Controller.Entities /// /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself + /// If the item is a folder, it returns the folder itself. /// [JsonIgnore] public virtual string ContainingFolderPath @@ -267,7 +267,7 @@ namespace MediaBrowser.Controller.Entities public string ServiceName { get; set; } /// - /// If this content came from an external service, the id of the content on that service + /// If this content came from an external service, the id of the content on that service. /// [JsonIgnore] public string ExternalId { get; set; } @@ -411,7 +411,7 @@ namespace MediaBrowser.Controller.Entities } /// - /// This is just a helper for convenience + /// This is just a helper for convenience. /// /// The primary image path. [JsonIgnore] @@ -556,7 +556,7 @@ namespace MediaBrowser.Controller.Entities public DateTime DateLastRefreshed { get; set; } /// - /// The logger + /// The logger. /// public static ILoggerFactory LoggerFactory { get; set; } public static ILogger Logger { get; set; } @@ -799,7 +799,7 @@ namespace MediaBrowser.Controller.Entities } /// - /// Finds a parent of a given type + /// Finds a parent of a given type. /// /// /// ``0. @@ -1351,7 +1351,7 @@ namespace MediaBrowser.Controller.Entities } /// - /// Overrides the base implementation to refresh metadata for local trailers + /// Overrides the base implementation to refresh metadata for local trailers. /// /// The options. /// The cancellation token. @@ -1753,7 +1753,7 @@ namespace MediaBrowser.Controller.Entities } /// - /// Determines if a given user has access to this item + /// Determines if a given user has access to this item. /// /// The user. /// true if [is parental allowed] [the specified user]; otherwise, false. @@ -2059,7 +2059,7 @@ namespace MediaBrowser.Controller.Entities public virtual bool EnableRememberingTrackSelections => true; /// - /// Adds a studio to the item + /// Adds a studio to the item. /// /// The name. /// @@ -2095,7 +2095,7 @@ namespace MediaBrowser.Controller.Entities } /// - /// Adds a genre to the item + /// Adds a genre to the item. /// /// The name. /// @@ -2190,7 +2190,7 @@ namespace MediaBrowser.Controller.Entities } /// - /// Gets an image + /// Gets an image. /// /// The type. /// Index of the image. @@ -2506,7 +2506,7 @@ namespace MediaBrowser.Controller.Entities } /// - /// Gets the file system path to delete when the item is to be deleted + /// Gets the file system path to delete when the item is to be deleted. /// /// public virtual IEnumerable GetDeletePaths() diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index e5adf88d1..35a6cef95 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -18,7 +18,7 @@ namespace MediaBrowser.Controller.Entities { /// /// Specialized Folder class that points to a subset of the physical folders in the system. - /// It is created from the user-specific folders within the system root + /// It is created from the user-specific folders within the system root. /// public class CollectionFolder : Folder, ICollectionFolder { @@ -140,7 +140,7 @@ namespace MediaBrowser.Controller.Entities } /// - /// Allow different display preferences for each collection folder + /// Allow different display preferences for each collection folder. /// /// The display prefs id. [JsonIgnore] diff --git a/MediaBrowser.Controller/Entities/Extensions.cs b/MediaBrowser.Controller/Entities/Extensions.cs index d2ca11740..3a34c668c 100644 --- a/MediaBrowser.Controller/Entities/Extensions.cs +++ b/MediaBrowser.Controller/Entities/Extensions.cs @@ -6,7 +6,7 @@ using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Entities { /// - /// Class Extensions + /// Class Extensions. /// public static class Extensions { diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 3a01b4379..34e5d78e7 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -31,7 +31,7 @@ using Series = MediaBrowser.Controller.Entities.TV.Series; namespace MediaBrowser.Controller.Entities { /// - /// Class Folder + /// Class Folder. /// public class Folder : BaseItem { @@ -172,7 +172,7 @@ namespace MediaBrowser.Controller.Entities public virtual IEnumerable Children => LoadChildren(); /// - /// thread-safe access to all recursive children of this folder - without regard to user + /// thread-safe access to all recursive children of this folder - without regard to user. /// /// The recursive children. [JsonIgnore] @@ -229,7 +229,7 @@ namespace MediaBrowser.Controller.Entities } /// - /// Validates that the children of the folder still exist + /// Validates that the children of the folder still exist. /// /// The progress. /// The cancellation token. @@ -570,7 +570,7 @@ namespace MediaBrowser.Controller.Entities } /// - /// Get the children of this folder from the actual file system + /// Get the children of this folder from the actual file system. /// /// IEnumerable{BaseItem}. protected virtual IEnumerable GetNonCachedChildren(IDirectoryService directoryService) @@ -582,7 +582,7 @@ namespace MediaBrowser.Controller.Entities } /// - /// Get our children from the repo - stubbed for now + /// Get our children from the repo - stubbed for now. /// /// IEnumerable{BaseItem}. protected List GetCachedChildren() @@ -1286,7 +1286,7 @@ namespace MediaBrowser.Controller.Entities } /// - /// Gets allowed recursive children of an item + /// Gets allowed recursive children of an item. /// /// The user. /// if set to true [include linked children]. diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs index 773c7df34..852cf4684 100644 --- a/MediaBrowser.Controller/Entities/Genre.cs +++ b/MediaBrowser.Controller/Entities/Genre.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities { /// - /// Class Genre + /// Class Genre. /// public class Genre : BaseItem, IItemByName { @@ -31,7 +31,7 @@ namespace MediaBrowser.Controller.Entities /// /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself + /// If the item is a folder, it returns the folder itself. /// /// The containing folder path. [JsonIgnore] @@ -96,7 +96,7 @@ namespace MediaBrowser.Controller.Entities } /// - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true or false indicating if changes were made. /// public override bool BeforeMetadataRefresh(bool replaceAllMetdata) { diff --git a/MediaBrowser.Controller/Entities/ICollectionFolder.cs b/MediaBrowser.Controller/Entities/ICollectionFolder.cs index 4f0760746..245b23ff0 100644 --- a/MediaBrowser.Controller/Entities/ICollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/ICollectionFolder.cs @@ -3,7 +3,7 @@ using System; namespace MediaBrowser.Controller.Entities { /// - /// This is just a marker interface to denote top level folders + /// This is just a marker interface to denote top level folders. /// public interface ICollectionFolder : IHasCollectionType { diff --git a/MediaBrowser.Controller/Entities/IHasAspectRatio.cs b/MediaBrowser.Controller/Entities/IHasAspectRatio.cs index 149c1e5ab..d7d007668 100644 --- a/MediaBrowser.Controller/Entities/IHasAspectRatio.cs +++ b/MediaBrowser.Controller/Entities/IHasAspectRatio.cs @@ -1,7 +1,7 @@ namespace MediaBrowser.Controller.Entities { /// - /// Interface IHasAspectRatio + /// Interface IHasAspectRatio. /// public interface IHasAspectRatio { diff --git a/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs index abee75a28..13226b234 100644 --- a/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs +++ b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs @@ -1,7 +1,7 @@ namespace MediaBrowser.Controller.Entities { /// - /// Interface IHasDisplayOrder + /// Interface IHasDisplayOrder. /// public interface IHasDisplayOrder { diff --git a/MediaBrowser.Controller/Entities/IHasScreenshots.cs b/MediaBrowser.Controller/Entities/IHasScreenshots.cs index 0975242f5..b027a0cb1 100644 --- a/MediaBrowser.Controller/Entities/IHasScreenshots.cs +++ b/MediaBrowser.Controller/Entities/IHasScreenshots.cs @@ -1,7 +1,7 @@ namespace MediaBrowser.Controller.Entities { /// - /// Interface IHasScreenshots + /// Interface IHasScreenshots. /// public interface IHasScreenshots { diff --git a/MediaBrowser.Controller/Entities/LinkedChild.cs b/MediaBrowser.Controller/Entities/LinkedChild.cs index d88c31007..fbce74aab 100644 --- a/MediaBrowser.Controller/Entities/LinkedChild.cs +++ b/MediaBrowser.Controller/Entities/LinkedChild.cs @@ -16,7 +16,7 @@ namespace MediaBrowser.Controller.Entities public string Id { get; set; } /// - /// Serves as a cache + /// Serves as a cache. /// public Guid? ItemId { get; set; } diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index be71bcc3c..70c48b6f1 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -11,7 +11,7 @@ using MediaBrowser.Model.Querying; namespace MediaBrowser.Controller.Entities.Movies { /// - /// Class BoxSet + /// Class BoxSet. /// public class BoxSet : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo { diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index 26a165025..53badac4d 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -13,7 +13,7 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Controller.Entities.Movies { /// - /// Class Movie + /// Class Movie. /// public class Movie : Video, IHasSpecialFeatures, IHasTrailers, IHasLookupInfo, ISupportsBoxSetGrouping { diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs index 9e4f9d47e..69bd7659c 100644 --- a/MediaBrowser.Controller/Entities/Person.cs +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -46,7 +46,7 @@ namespace MediaBrowser.Controller.Entities /// /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself + /// If the item is a folder, it returns the folder itself. /// /// The containing folder path. [JsonIgnore] @@ -118,7 +118,7 @@ namespace MediaBrowser.Controller.Entities } /// - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true or false indicating if changes were made. /// public override bool BeforeMetadataRefresh(bool replaceAllMetdata) { diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs index 068032317..f81b6f194 100644 --- a/MediaBrowser.Controller/Entities/Studio.cs +++ b/MediaBrowser.Controller/Entities/Studio.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities { /// - /// Class Studio + /// Class Studio. /// public class Studio : BaseItem, IItemByName { @@ -25,7 +25,7 @@ namespace MediaBrowser.Controller.Entities /// /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself + /// If the item is a folder, it returns the folder itself. /// /// The containing folder path. [JsonIgnore] @@ -97,7 +97,7 @@ namespace MediaBrowser.Controller.Entities } /// - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true or false indicating if changes were made. /// public override bool BeforeMetadataRefresh(bool replaceAllMetdata) { diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 4ec60e7cd..82e7d49fb 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -12,7 +12,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities.TV { /// - /// Class Episode + /// Class Episode. /// public class Episode : Video, IHasTrailers, IHasLookupInfo, IHasSeries { @@ -101,7 +101,7 @@ namespace MediaBrowser.Controller.Entities.TV } /// - /// This Episode's Series Instance + /// This Episode's Series Instance. /// /// The series. [JsonIgnore] diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 7dfd1a759..9f0658776 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -11,7 +11,7 @@ using MediaBrowser.Model.Querying; namespace MediaBrowser.Controller.Entities.TV { /// - /// Class Season + /// Class Season. /// public class Season : Folder, IHasSeries, IHasLookupInfo { @@ -68,7 +68,7 @@ namespace MediaBrowser.Controller.Entities.TV } /// - /// This Episode's Series Instance + /// This Episode's Series Instance. /// /// The series. [JsonIgnore] @@ -225,7 +225,7 @@ namespace MediaBrowser.Controller.Entities.TV } /// - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true or false indicating if changes were made. /// /// true if XXXX, false otherwise. public override bool BeforeMetadataRefresh(bool replaceAllMetdata) diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index a519089b3..9263a156a 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -17,7 +17,7 @@ using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider; namespace MediaBrowser.Controller.Entities.TV { /// - /// Class Series + /// Class Series. /// public class Series : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo, IMetadataContainer { @@ -54,7 +54,7 @@ namespace MediaBrowser.Controller.Entities.TV public IReadOnlyList RemoteTrailerIds { get; set; } /// - /// airdate, dvd or absolute + /// airdate, dvd or absolute. /// public string DisplayOrder { get; set; } diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs index c327d17c9..6b544afc6 100644 --- a/MediaBrowser.Controller/Entities/Trailer.cs +++ b/MediaBrowser.Controller/Entities/Trailer.cs @@ -9,7 +9,7 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Controller.Entities { /// - /// Class Trailer + /// Class Trailer. /// public class Trailer : Video, IHasLookupInfo { diff --git a/MediaBrowser.Controller/Entities/UserItemData.cs b/MediaBrowser.Controller/Entities/UserItemData.cs index ab425ee0f..81f4850a5 100644 --- a/MediaBrowser.Controller/Entities/UserItemData.cs +++ b/MediaBrowser.Controller/Entities/UserItemData.cs @@ -4,7 +4,7 @@ using System.Text.Json.Serialization; namespace MediaBrowser.Controller.Entities { /// - /// Class UserItemData + /// Class UserItemData. /// public class UserItemData { @@ -21,11 +21,11 @@ namespace MediaBrowser.Controller.Entities public string Key { get; set; } /// - /// The _rating + /// The _rating. /// private double? _rating; /// - /// Gets or sets the users 0-10 rating + /// Gets or sets the users 0-10 rating. /// /// The rating. /// Rating;A 0 to 10 rating is required for UserItemData. diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 4cfa0e74d..4efeede19 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -17,7 +17,7 @@ using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Controller.Entities { /// - /// Class Video + /// Class Video. /// public class Video : BaseItem, IHasAspectRatio, diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs index a01ef5c31..c961da643 100644 --- a/MediaBrowser.Controller/Entities/Year.cs +++ b/MediaBrowser.Controller/Entities/Year.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities { /// - /// Class Year + /// Class Year. /// public class Year : BaseItem, IItemByName { @@ -21,7 +21,7 @@ namespace MediaBrowser.Controller.Entities /// /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself + /// If the item is a folder, it returns the folder itself. /// /// The containing folder path. [JsonIgnore] @@ -107,7 +107,7 @@ namespace MediaBrowser.Controller.Entities } /// - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true or false indicating if changes were made. /// public override bool BeforeMetadataRefresh(bool replaceAllMetdata) { diff --git a/MediaBrowser.Controller/Extensions/StringExtensions.cs b/MediaBrowser.Controller/Extensions/StringExtensions.cs index b1aaf6534..e09543e14 100644 --- a/MediaBrowser.Controller/Extensions/StringExtensions.cs +++ b/MediaBrowser.Controller/Extensions/StringExtensions.cs @@ -7,7 +7,7 @@ using System.Text.RegularExpressions; namespace MediaBrowser.Controller.Extensions { /// - /// Class BaseExtensions + /// Class BaseExtensions. /// public static class StringExtensions { diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index d1d6c74b8..abdb0f695 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller { /// - /// Interface IServerApplicationHost + /// Interface IServerApplicationHost. /// public interface IServerApplicationHost : IApplicationHost { diff --git a/MediaBrowser.Controller/IServerApplicationPaths.cs b/MediaBrowser.Controller/IServerApplicationPaths.cs index c35a22ac7..155bf9177 100644 --- a/MediaBrowser.Controller/IServerApplicationPaths.cs +++ b/MediaBrowser.Controller/IServerApplicationPaths.cs @@ -5,7 +5,7 @@ namespace MediaBrowser.Controller public interface IServerApplicationPaths : IApplicationPaths { /// - /// Gets the path to the base root media directory + /// Gets the path to the base root media directory. /// /// The root folder path. string RootFolderPath { get; } @@ -17,13 +17,13 @@ namespace MediaBrowser.Controller string DefaultUserViewsPath { get; } /// - /// Gets the path to the People directory + /// Gets the path to the People directory. /// /// The people path. string PeoplePath { get; } /// - /// Gets the path to the Genre directory + /// Gets the path to the Genre directory. /// /// The genre path. string GenrePath { get; } @@ -35,25 +35,25 @@ namespace MediaBrowser.Controller string MusicGenrePath { get; } /// - /// Gets the path to the Studio directory + /// Gets the path to the Studio directory. /// /// The studio path. string StudioPath { get; } /// - /// Gets the path to the Year directory + /// Gets the path to the Year directory. /// /// The year path. string YearPath { get; } /// - /// Gets the path to the General IBN directory + /// Gets the path to the General IBN directory. /// /// The general path. string GeneralPath { get; } /// - /// Gets the path to the Ratings IBN directory + /// Gets the path to the Ratings IBN directory. /// /// The ratings path. string RatingsPath { get; } @@ -65,7 +65,7 @@ namespace MediaBrowser.Controller string MediaInfoImagesPath { get; } /// - /// Gets the path to the user configuration directory + /// Gets the path to the user configuration directory. /// /// The user configuration directory path. string UserConfigurationDirectoryPath { get; } diff --git a/MediaBrowser.Controller/Library/IIntroProvider.cs b/MediaBrowser.Controller/Library/IIntroProvider.cs index aa7001611..d45493d40 100644 --- a/MediaBrowser.Controller/Library/IIntroProvider.cs +++ b/MediaBrowser.Controller/Library/IIntroProvider.cs @@ -5,7 +5,7 @@ using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Library { /// - /// Class BaseIntroProvider + /// Class BaseIntroProvider. /// public interface IIntroProvider { diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index d7237039e..47c080ebd 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -21,7 +21,7 @@ using Person = MediaBrowser.Controller.Entities.Person; namespace MediaBrowser.Controller.Library { /// - /// Interface ILibraryManager + /// Interface ILibraryManager. /// public interface ILibraryManager { @@ -38,7 +38,7 @@ namespace MediaBrowser.Controller.Library bool allowIgnorePath = true); /// - /// Resolves a set of files into a list of BaseItem + /// Resolves a set of files into a list of BaseItem. /// IEnumerable ResolvePaths( IEnumerable files, @@ -54,7 +54,7 @@ namespace MediaBrowser.Controller.Library AggregateFolder RootFolder { get; } /// - /// Gets a Person + /// Gets a Person. /// /// The name. /// Task{Person}. @@ -75,14 +75,14 @@ namespace MediaBrowser.Controller.Library MusicArtist GetArtist(string name); MusicArtist GetArtist(string name, DtoOptions options); /// - /// Gets a Studio + /// Gets a Studio. /// /// The name. /// Task{Studio}. Studio GetStudio(string name); /// - /// Gets a Genre + /// Gets a Genre. /// /// The name. /// Task{Genre}. @@ -96,7 +96,7 @@ namespace MediaBrowser.Controller.Library MusicGenre GetMusicGenre(string name); /// - /// Gets a Year + /// Gets a Year. /// /// The value. /// Task{Year}. @@ -113,7 +113,7 @@ namespace MediaBrowser.Controller.Library Task ValidatePeople(CancellationToken cancellationToken, IProgress progress); /// - /// Reloads the root media folder + /// Reloads the root media folder. /// /// The progress. /// The cancellation token. diff --git a/MediaBrowser.Controller/Library/ILibraryPostScanTask.cs b/MediaBrowser.Controller/Library/ILibraryPostScanTask.cs index cba5e8fd7..4032e9d83 100644 --- a/MediaBrowser.Controller/Library/ILibraryPostScanTask.cs +++ b/MediaBrowser.Controller/Library/ILibraryPostScanTask.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; namespace MediaBrowser.Controller.Library { /// - /// An interface for tasks that run after the media library scan + /// An interface for tasks that run after the media library scan. /// public interface ILibraryPostScanTask { diff --git a/MediaBrowser.Controller/Library/IMetadataSaver.cs b/MediaBrowser.Controller/Library/IMetadataSaver.cs index dd119984e..027cc5b40 100644 --- a/MediaBrowser.Controller/Library/IMetadataSaver.cs +++ b/MediaBrowser.Controller/Library/IMetadataSaver.cs @@ -4,7 +4,7 @@ using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Library { /// - /// Interface IMetadataSaver + /// Interface IMetadataSaver. /// public interface IMetadataSaver { diff --git a/MediaBrowser.Controller/Library/ISearchEngine.cs b/MediaBrowser.Controller/Library/ISearchEngine.cs index 8498b92ae..31dcbba5b 100644 --- a/MediaBrowser.Controller/Library/ISearchEngine.cs +++ b/MediaBrowser.Controller/Library/ISearchEngine.cs @@ -4,7 +4,7 @@ using MediaBrowser.Model.Search; namespace MediaBrowser.Controller.Library { /// - /// Interface ILibrarySearchEngine + /// Interface ILibrarySearchEngine. /// public interface ISearchEngine { diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs index f5ccad671..d08ad4cac 100644 --- a/MediaBrowser.Controller/Library/IUserDataManager.cs +++ b/MediaBrowser.Controller/Library/IUserDataManager.cs @@ -42,14 +42,14 @@ namespace MediaBrowser.Controller.Library UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto itemDto, User user, DtoOptions dto_options); /// - /// Get all user data for the given user + /// Get all user data for the given user. /// /// /// List GetAllUserData(Guid userId); /// - /// Save the all provided user data for the given user + /// Save the all provided user data for the given user. /// /// /// @@ -58,7 +58,7 @@ namespace MediaBrowser.Controller.Library void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken); /// - /// Updates playstate for an item and returns true or false indicating if it was played to completion + /// Updates playstate for an item and returns true or false indicating if it was played to completion. /// bool UpdatePlayState(BaseItem item, UserItemData data, long? positionTicks); } diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index b5b2e4729..fe3e4f9e6 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -11,7 +11,7 @@ using MediaBrowser.Model.Users; namespace MediaBrowser.Controller.Library { /// - /// Interface IUserManager + /// Interface IUserManager. /// public interface IUserManager { diff --git a/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs b/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs index c9671de47..b5c48321b 100644 --- a/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs +++ b/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs @@ -3,7 +3,7 @@ using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Library { /// - /// Class ItemChangeEventArgs + /// Class ItemChangeEventArgs. /// public class ItemChangeEventArgs { diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs index cca85cd3b..7c1463862 100644 --- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs +++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs @@ -15,7 +15,7 @@ namespace MediaBrowser.Controller.Library public class ItemResolveArgs : EventArgs { /// - /// The _app paths + /// The _app paths. /// private readonly IServerApplicationPaths _appPaths; diff --git a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs index b4e205184..abe129484 100644 --- a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs +++ b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs @@ -8,7 +8,7 @@ using MediaBrowser.Model.Dto; namespace MediaBrowser.Controller.Library { /// - /// Holds information about a playback progress event + /// Holds information about a playback progress event. /// public class PlaybackProgressEventArgs : EventArgs { diff --git a/MediaBrowser.Controller/Library/Profiler.cs b/MediaBrowser.Controller/Library/Profiler.cs index 0febef3d3..a566de338 100644 --- a/MediaBrowser.Controller/Library/Profiler.cs +++ b/MediaBrowser.Controller/Library/Profiler.cs @@ -5,21 +5,21 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Library { /// - /// Class Profiler + /// Class Profiler. /// public class Profiler : IDisposable { /// - /// The name + /// The name. /// readonly string _name; /// - /// The stopwatch + /// The stopwatch. /// readonly Stopwatch _stopwatch; /// - /// The _logger + /// The _logger. /// private readonly ILogger _logger; diff --git a/MediaBrowser.Controller/Library/SearchHintInfo.cs b/MediaBrowser.Controller/Library/SearchHintInfo.cs index 692431e34..897c2b7f4 100644 --- a/MediaBrowser.Controller/Library/SearchHintInfo.cs +++ b/MediaBrowser.Controller/Library/SearchHintInfo.cs @@ -3,7 +3,7 @@ using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Library { /// - /// Class SearchHintInfo + /// Class SearchHintInfo. /// public class SearchHintInfo { diff --git a/MediaBrowser.Controller/Library/TVUtils.cs b/MediaBrowser.Controller/Library/TVUtils.cs index fd5fb6748..ebdcfbdb3 100644 --- a/MediaBrowser.Controller/Library/TVUtils.cs +++ b/MediaBrowser.Controller/Library/TVUtils.cs @@ -3,7 +3,7 @@ using System; namespace MediaBrowser.Controller.Library { /// - /// Class TVUtils + /// Class TVUtils. /// public static class TVUtils { diff --git a/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs b/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs index 3e7351b8b..fa0192784 100644 --- a/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs +++ b/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs @@ -6,7 +6,7 @@ using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Library { /// - /// Class UserDataSaveEventArgs + /// Class UserDataSaveEventArgs. /// public class UserDataSaveEventArgs : EventArgs { diff --git a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs index 70477fce7..1b7095c7b 100644 --- a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs +++ b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs @@ -3,7 +3,7 @@ using MediaBrowser.Model.LiveTv; namespace MediaBrowser.Controller.LiveTv { /// - /// Class ChannelInfo + /// Class ChannelInfo. /// public class ChannelInfo { @@ -44,13 +44,13 @@ namespace MediaBrowser.Controller.LiveTv public ChannelType ChannelType { get; set; } /// - /// Supply the image path if it can be accessed directly from the file system + /// Supply the image path if it can be accessed directly from the file system. /// /// The image path. public string ImagePath { get; set; } /// - /// Supply the image url if it can be downloaded + /// Supply the image url if it can be downloaded. /// /// The image URL. public string ImageUrl { get; set; } diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index bc3bf78f0..aa24d5ab8 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -14,7 +14,7 @@ using MediaBrowser.Model.Querying; namespace MediaBrowser.Controller.LiveTv { /// - /// Manages all live tv services installed on the server + /// Manages all live tv services installed on the server. /// public interface ILiveTvManager { diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs index e17db34c6..472b061e6 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs @@ -140,7 +140,7 @@ namespace MediaBrowser.Controller.LiveTv /// /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself + /// If the item is a folder, it returns the folder itself. /// /// The containing folder path. [JsonIgnore] diff --git a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs index 5d0f13192..b847140f5 100644 --- a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs +++ b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs @@ -18,7 +18,7 @@ namespace MediaBrowser.Controller.LiveTv public string ChannelId { get; set; } /// - /// Name of the program + /// Name of the program. /// public string Name { get; set; } @@ -95,13 +95,13 @@ namespace MediaBrowser.Controller.LiveTv public string EpisodeTitle { get; set; } /// - /// Supply the image path if it can be accessed directly from the file system + /// Supply the image path if it can be accessed directly from the file system. /// /// The image path. public string ImagePath { get; set; } /// - /// Supply the image url if it can be downloaded + /// Supply the image url if it can be downloaded. /// /// The image URL. public string ImageUrl { get; set; } diff --git a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs index 432388d6b..b9e0218ab 100644 --- a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs +++ b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs @@ -169,13 +169,13 @@ namespace MediaBrowser.Controller.LiveTv public float? CommunityRating { get; set; } /// - /// Supply the image path if it can be accessed directly from the file system + /// Supply the image path if it can be accessed directly from the file system. /// /// The image path. public string ImagePath { get; set; } /// - /// Supply the image url if it can be downloaded + /// Supply the image url if it can be downloaded. /// /// The image URL. public string ImageUrl { get; set; } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 0ca42c0e0..680d92c2d 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -108,7 +108,7 @@ namespace MediaBrowser.Controller.MediaEncoding } /// - /// Gets the name of the output video codec + /// Gets the name of the output video codec. /// public string GetVideoEncoder(EncodingJobInfo state, EncodingOptions encodingOptions) { @@ -285,7 +285,7 @@ namespace MediaBrowser.Controller.MediaEncoding } /// - /// Infers the audio codec based on the url + /// Infers the audio codec based on the url. /// public string InferAudioCodec(string container) { @@ -703,7 +703,7 @@ namespace MediaBrowser.Controller.MediaEncoding } /// - /// Gets the video bitrate to specify on the command line + /// Gets the video bitrate to specify on the command line. /// public string GetVideoQualityParam(EncodingJobInfo state, string videoEncoder, EncodingOptions encodingOptions, string defaultPreset) { @@ -1300,7 +1300,7 @@ namespace MediaBrowser.Controller.MediaEncoding } /// - /// Gets the number of audio channels to specify on the command line + /// Gets the number of audio channels to specify on the command line. /// /// The state. /// The audio stream. @@ -1486,7 +1486,7 @@ namespace MediaBrowser.Controller.MediaEncoding } /// - /// Determines which stream will be used for playback + /// Determines which stream will be used for playback. /// /// All stream. /// Index of the desired. @@ -1961,7 +1961,7 @@ namespace MediaBrowser.Controller.MediaEncoding } /// - /// If we're going to put a fixed size on the command line, this will calculate it + /// If we're going to put a fixed size on the command line, this will calculate it. /// public string GetOutputSizeParam( EncodingJobInfo state, @@ -2505,7 +2505,7 @@ namespace MediaBrowser.Controller.MediaEncoding } /// - /// Gets the name of the output video codec + /// Gets the name of the output video codec. /// protected string GetHardwareAcceleratedVideoDecoder(EncodingJobInfo state, EncodingOptions encodingOptions) { diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index acf1aae89..32973619d 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -418,7 +418,7 @@ namespace MediaBrowser.Controller.MediaEncoding } /// - /// Predicts the audio sample rate that will be in the output stream + /// Predicts the audio sample rate that will be in the output stream. /// public double? TargetVideoLevel { @@ -441,7 +441,7 @@ namespace MediaBrowser.Controller.MediaEncoding } /// - /// Predicts the audio sample rate that will be in the output stream + /// Predicts the audio sample rate that will be in the output stream. /// public int? TargetVideoBitDepth { @@ -476,7 +476,7 @@ namespace MediaBrowser.Controller.MediaEncoding } /// - /// Predicts the audio sample rate that will be in the output stream + /// Predicts the audio sample rate that will be in the output stream. /// public float? TargetFramerate { @@ -508,7 +508,7 @@ namespace MediaBrowser.Controller.MediaEncoding } /// - /// Predicts the audio sample rate that will be in the output stream + /// Predicts the audio sample rate that will be in the output stream. /// public int? TargetPacketLength { @@ -524,7 +524,7 @@ namespace MediaBrowser.Controller.MediaEncoding } /// - /// Predicts the audio sample rate that will be in the output stream + /// Predicts the audio sample rate that will be in the output stream. /// public string TargetVideoProfile { @@ -679,20 +679,20 @@ namespace MediaBrowser.Controller.MediaEncoding } /// - /// Enum TranscodingJobType + /// Enum TranscodingJobType. /// public enum TranscodingJobType { /// - /// The progressive + /// The progressive. /// Progressive, /// - /// The HLS + /// The HLS. /// Hls, /// - /// The dash + /// The dash. /// Dash } diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index 37f0b11a7..e33c4ad0b 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -11,7 +11,7 @@ using MediaBrowser.Model.System; namespace MediaBrowser.Controller.MediaEncoding { /// - /// Interface IMediaEncoder + /// Interface IMediaEncoder. /// public interface IMediaEncoder : ITranscoderSupport { diff --git a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs index 5cedc3d57..6c9bbb043 100644 --- a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs +++ b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs @@ -7,7 +7,7 @@ using MediaBrowser.Model.IO; namespace MediaBrowser.Controller.MediaEncoding { /// - /// Class MediaEncoderHelpers + /// Class MediaEncoderHelpers. /// public static class MediaEncoderHelpers { diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index df90c399b..9e262d31e 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -11,7 +11,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Net { /// - /// Starts sending data over a web socket periodically when a message is received, and then stops when a corresponding stop message is received + /// Starts sending data over a web socket periodically when a message is received, and then stops when a corresponding stop message is received. /// /// The type of the T return data type. /// The type of the T state type. @@ -20,7 +20,7 @@ namespace MediaBrowser.Controller.Net where TReturnDataType : class { /// - /// The _active connections + /// The _active connections. /// private readonly List> _activeConnections = new List>(); @@ -38,7 +38,7 @@ namespace MediaBrowser.Controller.Net protected abstract Task GetDataToSend(); /// - /// The logger + /// The logger. /// protected ILogger> Logger; @@ -78,7 +78,7 @@ namespace MediaBrowser.Controller.Net } /// - /// Starts sending messages over a web socket + /// Starts sending messages over a web socket. /// /// The message. private void Start(WebSocketMessageInfo message) @@ -180,7 +180,7 @@ namespace MediaBrowser.Controller.Net } /// - /// Stops sending messages over a web socket + /// Stops sending messages over a web socket. /// /// The message. private void Stop(WebSocketMessageInfo message) diff --git a/MediaBrowser.Controller/Net/IHttpResultFactory.cs b/MediaBrowser.Controller/Net/IHttpResultFactory.cs index 25404fa78..609bd5f59 100644 --- a/MediaBrowser.Controller/Net/IHttpResultFactory.cs +++ b/MediaBrowser.Controller/Net/IHttpResultFactory.cs @@ -7,7 +7,7 @@ using MediaBrowser.Model.Services; namespace MediaBrowser.Controller.Net { /// - /// Interface IHttpResultFactory + /// Interface IHttpResultFactory. /// public interface IHttpResultFactory { diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs index efb5f4ac3..e6609fae3 100644 --- a/MediaBrowser.Controller/Net/IHttpServer.cs +++ b/MediaBrowser.Controller/Net/IHttpServer.cs @@ -29,19 +29,19 @@ namespace MediaBrowser.Controller.Net void Init(IEnumerable serviceTypes, IEnumerable listener, IEnumerable urlPrefixes); /// - /// If set, all requests will respond with this message + /// If set, all requests will respond with this message. /// string GlobalResponse { get; set; } /// - /// The HTTP request handler + /// The HTTP request handler. /// /// /// Task RequestHandler(HttpContext context); /// - /// Get the default CORS headers + /// Get the default CORS headers. /// /// /// diff --git a/MediaBrowser.Controller/Net/IWebSocketListener.cs b/MediaBrowser.Controller/Net/IWebSocketListener.cs index 0f472a2bc..7250a57b0 100644 --- a/MediaBrowser.Controller/Net/IWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/IWebSocketListener.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; namespace MediaBrowser.Controller.Net { /// - ///This is an interface for listening to messages coming through a web socket connection + ///This is an interface for listening to messages coming through a web socket connection. /// public interface IWebSocketListener { diff --git a/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs index 5bf39cae6..be0b3ddc3 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs @@ -3,7 +3,7 @@ using MediaBrowser.Model.Net; namespace MediaBrowser.Controller.Net { /// - /// Class WebSocketMessageInfo + /// Class WebSocketMessageInfo. /// public class WebSocketMessageInfo : WebSocketMessage { diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 75fc43a04..0ae1b8bbf 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -9,12 +9,12 @@ using MediaBrowser.Model.Querying; namespace MediaBrowser.Controller.Persistence { /// - /// Provides an interface to implement an Item repository + /// Provides an interface to implement an Item repository. /// public interface IItemRepository : IRepository { /// - /// Saves an item + /// Saves an item. /// /// The item. /// The cancellation token. @@ -43,14 +43,14 @@ namespace MediaBrowser.Controller.Persistence BaseItem RetrieveItem(Guid id); /// - /// Gets chapters for an item + /// Gets chapters for an item. /// /// /// List GetChapters(BaseItem id); /// - /// Gets a single chapter for an item + /// Gets a single chapter for an item. /// /// /// diff --git a/MediaBrowser.Controller/Persistence/IRepository.cs b/MediaBrowser.Controller/Persistence/IRepository.cs index 56bf1dd5a..42f285076 100644 --- a/MediaBrowser.Controller/Persistence/IRepository.cs +++ b/MediaBrowser.Controller/Persistence/IRepository.cs @@ -3,12 +3,12 @@ using System; namespace MediaBrowser.Controller.Persistence { /// - /// Provides a base interface for all the repository interfaces + /// Provides a base interface for all the repository interfaces. /// public interface IRepository : IDisposable { /// - /// Gets the name of the repository + /// Gets the name of the repository. /// /// The name. string Name { get; } diff --git a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs index 4c327eeef..ba7c9fd50 100644 --- a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs +++ b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs @@ -5,7 +5,7 @@ using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Persistence { /// - /// Provides an interface to implement a UserData repository + /// Provides an interface to implement a UserData repository. /// public interface IUserDataRepository : IRepository { @@ -30,14 +30,14 @@ namespace MediaBrowser.Controller.Persistence UserItemData GetUserData(long userId, List keys); /// - /// Return all user data associated with the given user + /// Return all user data associated with the given user. /// /// /// List GetAllUserData(long userId); /// - /// Save all user data associated with the given user + /// Save all user data associated with the given user. /// /// /// diff --git a/MediaBrowser.Controller/Plugins/IPluginConfigurationPage.cs b/MediaBrowser.Controller/Plugins/IPluginConfigurationPage.cs index c156da924..077f5ab63 100644 --- a/MediaBrowser.Controller/Plugins/IPluginConfigurationPage.cs +++ b/MediaBrowser.Controller/Plugins/IPluginConfigurationPage.cs @@ -4,7 +4,7 @@ using MediaBrowser.Common.Plugins; namespace MediaBrowser.Controller.Plugins { /// - /// Interface IConfigurationPage + /// Interface IConfigurationPage. /// public interface IPluginConfigurationPage { @@ -34,16 +34,16 @@ namespace MediaBrowser.Controller.Plugins } /// - /// Enum ConfigurationPageType + /// Enum ConfigurationPageType. /// public enum ConfigurationPageType { /// - /// The plugin configuration + /// The plugin configuration. /// PluginConfiguration, /// - /// The none + /// The none. /// None } diff --git a/MediaBrowser.Controller/Providers/IMetadataProvider.cs b/MediaBrowser.Controller/Providers/IMetadataProvider.cs index 3e595ff93..62b16dadd 100644 --- a/MediaBrowser.Controller/Providers/IMetadataProvider.cs +++ b/MediaBrowser.Controller/Providers/IMetadataProvider.cs @@ -3,7 +3,7 @@ using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Providers { /// - /// Marker interface + /// Marker interface. /// public interface IMetadataProvider { diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshMode.cs b/MediaBrowser.Controller/Providers/MetadataRefreshMode.cs index 02152ee33..6d49b5510 100644 --- a/MediaBrowser.Controller/Providers/MetadataRefreshMode.cs +++ b/MediaBrowser.Controller/Providers/MetadataRefreshMode.cs @@ -3,22 +3,22 @@ namespace MediaBrowser.Controller.Providers public enum MetadataRefreshMode { /// - /// The none + /// The none. /// None = 0, /// - /// The validation only + /// The validation only. /// ValidationOnly = 1, /// - /// Providers will be executed based on default rules + /// Providers will be executed based on default rules. /// Default = 2, /// - /// All providers will be executed to search for new metadata + /// All providers will be executed to search for new metadata. /// FullRefresh = 3 } diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs index 59adaedfa..83efed2a1 100644 --- a/MediaBrowser.Controller/Providers/MetadataResult.cs +++ b/MediaBrowser.Controller/Providers/MetadataResult.cs @@ -40,7 +40,7 @@ namespace MediaBrowser.Controller.Providers } /// - /// Not only does this clear, but initializes the list so that services can differentiate between a null list and zero people + /// Not only does this clear, but initializes the list so that services can differentiate between a null list and zero people. /// public void ResetPeople() { diff --git a/MediaBrowser.Controller/Providers/VideoContentType.cs b/MediaBrowser.Controller/Providers/VideoContentType.cs index c3b8964a3..49d587f6c 100644 --- a/MediaBrowser.Controller/Providers/VideoContentType.cs +++ b/MediaBrowser.Controller/Providers/VideoContentType.cs @@ -1,17 +1,17 @@ namespace MediaBrowser.Controller.Providers { /// - /// Enum VideoContentType + /// Enum VideoContentType. /// public enum VideoContentType { /// - /// The episode + /// The episode. /// Episode = 0, /// - /// The movie + /// The movie. /// Movie = 1 } diff --git a/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs b/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs index 637a7e3f0..67acdd9a3 100644 --- a/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs +++ b/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs @@ -4,7 +4,7 @@ using MediaBrowser.Controller.Library; namespace MediaBrowser.Controller.Resolvers { /// - /// Class ItemResolver + /// Class ItemResolver. /// /// public abstract class ItemResolver : IItemResolver @@ -27,7 +27,7 @@ namespace MediaBrowser.Controller.Resolvers public virtual ResolverPriority Priority => ResolverPriority.First; /// - /// Sets initial values on the newly resolved item + /// Sets initial values on the newly resolved item. /// /// The item. /// The args. diff --git a/MediaBrowser.Controller/Resolvers/IItemResolver.cs b/MediaBrowser.Controller/Resolvers/IItemResolver.cs index 16e37d249..2e82b51f1 100644 --- a/MediaBrowser.Controller/Resolvers/IItemResolver.cs +++ b/MediaBrowser.Controller/Resolvers/IItemResolver.cs @@ -7,7 +7,7 @@ using MediaBrowser.Model.IO; namespace MediaBrowser.Controller.Resolvers { /// - /// Interface IItemResolver + /// Interface IItemResolver. /// public interface IItemResolver { diff --git a/MediaBrowser.Controller/Resolvers/ResolverPriority.cs b/MediaBrowser.Controller/Resolvers/ResolverPriority.cs index e39310095..1911e5c1d 100644 --- a/MediaBrowser.Controller/Resolvers/ResolverPriority.cs +++ b/MediaBrowser.Controller/Resolvers/ResolverPriority.cs @@ -1,25 +1,25 @@ namespace MediaBrowser.Controller.Resolvers { /// - /// Enum ResolverPriority + /// Enum ResolverPriority. /// public enum ResolverPriority { /// - /// The first + /// The first. /// First = 1, /// - /// The second + /// The second. /// Second = 2, /// - /// The third + /// The third. /// Third = 3, Fourth = 4, /// - /// The last + /// The last. /// Last = 5 } diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 1fdb588eb..e54f21050 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -13,7 +13,7 @@ using MediaBrowser.Model.SyncPlay; namespace MediaBrowser.Controller.Session { /// - /// Interface ISessionManager + /// Interface ISessionManager. /// public interface ISessionManager { @@ -79,14 +79,14 @@ namespace MediaBrowser.Controller.Session void UpdateDeviceName(string sessionId, string reportedDeviceName); /// - /// Used to report that playback has started for an item + /// Used to report that playback has started for an item. /// /// The info. /// Task. Task OnPlaybackStart(PlaybackStartInfo info); /// - /// Used to report playback progress for an item + /// Used to report playback progress for an item. /// /// The info. /// Task. @@ -96,7 +96,7 @@ namespace MediaBrowser.Controller.Session Task OnPlaybackProgress(PlaybackProgressInfo info, bool isAutomated); /// - /// Used to report that playback has ended for an item + /// Used to report that playback has ended for an item. /// /// The info. /// Task. diff --git a/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs index 31087edec..727cbe639 100644 --- a/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs +++ b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs @@ -4,7 +4,7 @@ using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Sorting { /// - /// Interface IBaseItemComparer + /// Interface IBaseItemComparer. /// public interface IBaseItemComparer : IComparer { diff --git a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs index 6f75d16de..6d03d97ae 100644 --- a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs +++ b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs @@ -3,7 +3,7 @@ using MediaBrowser.Controller.Library; namespace MediaBrowser.Controller.Sorting { /// - /// Represents a BaseItem comparer that requires a User to perform it's comparison + /// Represents a BaseItem comparer that requires a User to perform it's comparison. /// public interface IUserBaseItemComparer : IBaseItemComparer { diff --git a/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs b/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs index c0b62b753..b2c53365c 100644 --- a/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs +++ b/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs @@ -1,7 +1,7 @@ namespace MediaBrowser.Controller.Sync { /// - /// A marker interface + /// A marker interface. /// public interface IRemoteSyncProvider { diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index 0ceb55c57..da5a3d5bc 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -14,14 +14,14 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.LocalMetadata.Parsers { /// - /// Provides a base class for parsing metadata xml + /// Provides a base class for parsing metadata xml. /// /// public class BaseItemXmlParser where T : BaseItem { /// - /// The logger + /// The logger. /// protected ILogger> Logger { get; private set; } protected IProviderManager ProviderManager { get; private set; } @@ -39,7 +39,7 @@ namespace MediaBrowser.LocalMetadata.Parsers } /// - /// Fetches metadata for an item from one xml file + /// Fetches metadata for an item from one xml file. /// /// The item. /// The metadata file. @@ -124,7 +124,7 @@ namespace MediaBrowser.LocalMetadata.Parsers private readonly CultureInfo _usCulture = new CultureInfo("en-US"); /// - /// Fetches metadata from one Xml Element + /// Fetches metadata from one Xml Element. /// /// The reader. /// The item result. @@ -1230,7 +1230,7 @@ namespace MediaBrowser.LocalMetadata.Parsers /// - /// Used to split names of comma or pipe delimeted genres and people + /// Used to split names of comma or pipe delimeted genres and people. /// /// The value. /// IEnumerable{System.String}. diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs index 3260f3051..e6359f4fb 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs @@ -9,7 +9,7 @@ using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.MediaEncoding.BdInfo { /// - /// Class BdInfoExaminer + /// Class BdInfoExaminer. /// public class BdInfoExaminer : IBlurayExaminer { diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index a4896d5f9..59fd6b848 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -25,7 +25,7 @@ using System.Diagnostics; namespace MediaBrowser.MediaEncoding.Encoder { /// - /// Class MediaEncoder + /// Class MediaEncoder. /// public class MediaEncoder : IMediaEncoder, IDisposable { @@ -425,7 +425,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } /// - /// The us culture + /// The us culture. /// protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); diff --git a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs index 78dc7b607..3aa296f7f 100644 --- a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs +++ b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs @@ -35,7 +35,7 @@ namespace MediaBrowser.MediaEncoding.Probing } /// - /// Gets a string from an FFProbeResult tags dictionary + /// Gets a string from an FFProbeResult tags dictionary. /// /// The tags. /// The key. @@ -52,7 +52,7 @@ namespace MediaBrowser.MediaEncoding.Probing } /// - /// Gets an int from an FFProbeResult tags dictionary + /// Gets an int from an FFProbeResult tags dictionary. /// /// The tags. /// The key. @@ -73,7 +73,7 @@ namespace MediaBrowser.MediaEncoding.Probing } /// - /// Gets a DateTime from an FFProbeResult tags dictionary + /// Gets a DateTime from an FFProbeResult tags dictionary. /// /// The tags. /// The key. @@ -94,7 +94,7 @@ namespace MediaBrowser.MediaEncoding.Probing } /// - /// Converts a dictionary to case insensitive + /// Converts a dictionary to case insensitive. /// /// The dict. /// Dictionary{System.StringSystem.String}. diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 7d57a691e..4c48c038f 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -515,7 +515,7 @@ namespace MediaBrowser.MediaEncoding.Probing } /// - /// Converts ffprobe stream info to our MediaAttachment class + /// Converts ffprobe stream info to our MediaAttachment class. /// /// The stream info. /// MediaAttachments. @@ -548,7 +548,7 @@ namespace MediaBrowser.MediaEncoding.Probing } /// - /// Converts ffprobe stream info to our MediaStream class + /// Converts ffprobe stream info to our MediaStream class. /// /// if set to true [is info]. /// The stream info. @@ -767,7 +767,7 @@ namespace MediaBrowser.MediaEncoding.Probing } /// - /// Gets a string from an FFProbeResult tags dictionary + /// Gets a string from an FFProbeResult tags dictionary. /// /// The tags. /// The key. @@ -1154,7 +1154,7 @@ namespace MediaBrowser.MediaEncoding.Probing } /// - /// Gets the studios from the tags collection + /// Gets the studios from the tags collection. /// /// The info. /// The tags. @@ -1191,7 +1191,7 @@ namespace MediaBrowser.MediaEncoding.Probing } /// - /// Gets the genres from the tags collection + /// Gets the genres from the tags collection. /// /// The information. /// The tags. diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 7e9894f0a..763d7f641 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -344,7 +344,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles } /// - /// The _semaphoreLocks + /// The _semaphoreLocks. /// private readonly ConcurrentDictionary _semaphoreLocks = new ConcurrentDictionary(); diff --git a/MediaBrowser.Model/Channels/ChannelFeatures.cs b/MediaBrowser.Model/Channels/ChannelFeatures.cs index 496102d83..a55754edd 100644 --- a/MediaBrowser.Model/Channels/ChannelFeatures.cs +++ b/MediaBrowser.Model/Channels/ChannelFeatures.cs @@ -38,7 +38,7 @@ namespace MediaBrowser.Model.Channels public ChannelMediaContentType[] ContentTypes { get; set; } /// - /// Represents the maximum number of records the channel allows retrieving at a time + /// Represents the maximum number of records the channel allows retrieving at a time. /// public int? MaxPageSize { get; set; } diff --git a/MediaBrowser.Model/Channels/ChannelQuery.cs b/MediaBrowser.Model/Channels/ChannelQuery.cs index d11260039..fd90e7f06 100644 --- a/MediaBrowser.Model/Channels/ChannelQuery.cs +++ b/MediaBrowser.Model/Channels/ChannelQuery.cs @@ -10,7 +10,7 @@ namespace MediaBrowser.Model.Channels public class ChannelQuery { /// - /// Fields to return within the items, in addition to basic information + /// Fields to return within the items, in addition to basic information. /// /// The fields. public ItemFields[] Fields { get; set; } @@ -34,7 +34,7 @@ namespace MediaBrowser.Model.Channels public int? StartIndex { get; set; } /// - /// The maximum number of items to return + /// The maximum number of items to return. /// /// The limit. public int? Limit { get; set; } diff --git a/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs b/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs index cdd322c94..54f4fb293 100644 --- a/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs +++ b/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs @@ -12,7 +12,7 @@ namespace MediaBrowser.Model.Configuration public class BaseApplicationConfiguration { /// - /// The number of days we should retain log files + /// The number of days we should retain log files. /// /// The log file retention days. public int LogFileRetentionDays { get; set; } diff --git a/MediaBrowser.Model/Configuration/MetadataPluginType.cs b/MediaBrowser.Model/Configuration/MetadataPluginType.cs index bff12799f..4c5e95266 100644 --- a/MediaBrowser.Model/Configuration/MetadataPluginType.cs +++ b/MediaBrowser.Model/Configuration/MetadataPluginType.cs @@ -3,7 +3,7 @@ namespace MediaBrowser.Model.Configuration { /// - /// Enum MetadataPluginType + /// Enum MetadataPluginType. /// public enum MetadataPluginType { diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index afbe02dd3..742887620 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -111,19 +111,19 @@ namespace MediaBrowser.Model.Configuration public string MetadataCountryCode { get; set; } /// - /// Characters to be replaced with a ' ' in strings to create a sort name + /// Characters to be replaced with a ' ' in strings to create a sort name. /// /// The sort replace characters. public string[] SortReplaceCharacters { get; set; } /// - /// Characters to be removed from strings to create a sort name + /// Characters to be removed from strings to create a sort name. /// /// The sort remove characters. public string[] SortRemoveCharacters { get; set; } /// - /// Words to be removed from strings to create a sort name + /// Words to be removed from strings to create a sort name. /// /// The sort remove words. public string[] SortRemoveWords { get; set; } diff --git a/MediaBrowser.Model/Configuration/UserConfiguration.cs b/MediaBrowser.Model/Configuration/UserConfiguration.cs index 85d864eec..d236ac215 100644 --- a/MediaBrowser.Model/Configuration/UserConfiguration.cs +++ b/MediaBrowser.Model/Configuration/UserConfiguration.cs @@ -7,7 +7,7 @@ using Jellyfin.Data.Enums; namespace MediaBrowser.Model.Configuration { /// - /// Class UserConfiguration + /// Class UserConfiguration. /// public class UserConfiguration { diff --git a/MediaBrowser.Model/Dlna/AudioOptions.cs b/MediaBrowser.Model/Dlna/AudioOptions.cs index fc555c5f7..baa99b903 100644 --- a/MediaBrowser.Model/Dlna/AudioOptions.cs +++ b/MediaBrowser.Model/Dlna/AudioOptions.cs @@ -47,7 +47,7 @@ namespace MediaBrowser.Model.Dlna public int? MaxAudioChannels { get; set; } /// - /// The application's configured quality setting + /// The application's configured quality setting. /// public long? MaxBitrate { get; set; } diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 244463803..28d8a6439 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -465,7 +465,7 @@ namespace MediaBrowser.Model.Dlna } /// - /// Returns the audio stream that will be used + /// Returns the audio stream that will be used. /// public MediaStream TargetAudioStream { @@ -481,7 +481,7 @@ namespace MediaBrowser.Model.Dlna } /// - /// Returns the video stream that will be used + /// Returns the video stream that will be used. /// public MediaStream TargetVideoStream { @@ -497,7 +497,7 @@ namespace MediaBrowser.Model.Dlna } /// - /// Predicts the audio sample rate that will be in the output stream + /// Predicts the audio sample rate that will be in the output stream. /// public int? TargetAudioSampleRate { @@ -509,7 +509,7 @@ namespace MediaBrowser.Model.Dlna } /// - /// Predicts the audio sample rate that will be in the output stream + /// Predicts the audio sample rate that will be in the output stream. /// public int? TargetAudioBitDepth { @@ -532,7 +532,7 @@ namespace MediaBrowser.Model.Dlna } /// - /// Predicts the audio sample rate that will be in the output stream + /// Predicts the audio sample rate that will be in the output stream. /// public int? TargetVideoBitDepth { @@ -579,7 +579,7 @@ namespace MediaBrowser.Model.Dlna } /// - /// Predicts the audio sample rate that will be in the output stream + /// Predicts the audio sample rate that will be in the output stream. /// public float? TargetFramerate { @@ -593,7 +593,7 @@ namespace MediaBrowser.Model.Dlna } /// - /// Predicts the audio sample rate that will be in the output stream + /// Predicts the audio sample rate that will be in the output stream. /// public double? TargetVideoLevel { @@ -680,7 +680,7 @@ namespace MediaBrowser.Model.Dlna } /// - /// Predicts the audio sample rate that will be in the output stream + /// Predicts the audio sample rate that will be in the output stream. /// public int? TargetPacketLength { @@ -694,7 +694,7 @@ namespace MediaBrowser.Model.Dlna } /// - /// Predicts the audio sample rate that will be in the output stream + /// Predicts the audio sample rate that will be in the output stream. /// public string TargetVideoProfile { @@ -732,7 +732,7 @@ namespace MediaBrowser.Model.Dlna } /// - /// Predicts the audio bitrate that will be in the output stream + /// Predicts the audio bitrate that will be in the output stream. /// public int? TargetAudioBitrate { @@ -746,7 +746,7 @@ namespace MediaBrowser.Model.Dlna } /// - /// Predicts the audio channels that will be in the output stream + /// Predicts the audio channels that will be in the output stream. /// public int? TargetAudioChannels { @@ -787,7 +787,7 @@ namespace MediaBrowser.Model.Dlna } /// - /// Predicts the audio codec that will be in the output stream + /// Predicts the audio codec that will be in the output stream. /// public string[] TargetAudioCodec { @@ -840,7 +840,7 @@ namespace MediaBrowser.Model.Dlna } /// - /// Predicts the audio channels that will be in the output stream + /// Predicts the audio channels that will be in the output stream. /// public long? TargetSize { diff --git a/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs b/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs index 7b0204590..e7fe8d6af 100644 --- a/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs +++ b/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs @@ -5,22 +5,22 @@ namespace MediaBrowser.Model.Dlna public enum SubtitleDeliveryMethod { /// - /// The encode + /// The encode. /// Encode = 0, /// - /// The embed + /// The embed. /// Embed = 1, /// - /// The external + /// The external. /// External = 2, /// - /// The HLS + /// The HLS. /// Hls = 3 } diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index c7f8f0584..cad18b5c8 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -308,7 +308,7 @@ namespace MediaBrowser.Model.Dto public int? LocalTrailerCount { get; set; } /// - /// User data for this item based on the user it's being requested for + /// User data for this item based on the user it's being requested for. /// /// The user data. public UserItemDataDto UserData { get; set; } diff --git a/MediaBrowser.Model/Dto/ImageOptions.cs b/MediaBrowser.Model/Dto/ImageOptions.cs index 158e622a8..3f4405f1e 100644 --- a/MediaBrowser.Model/Dto/ImageOptions.cs +++ b/MediaBrowser.Model/Dto/ImageOptions.cs @@ -61,7 +61,7 @@ namespace MediaBrowser.Model.Dto /// /// Gets or sets the image tag. - /// If set this will result in strong, unconditional response caching + /// If set this will result in strong, unconditional response caching. /// /// The hash. public string Tag { get; set; } diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs index 74c2cb4f4..e78e60d52 100644 --- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs +++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs @@ -28,7 +28,7 @@ namespace MediaBrowser.Model.Dto public string Name { get; set; } /// - /// Differentiate internet url vs local network + /// Differentiate internet url vs local network. /// public bool IsRemote { get; set; } diff --git a/MediaBrowser.Model/Entities/DisplayPreferences.cs b/MediaBrowser.Model/Entities/DisplayPreferences.cs index 0e5db01dd..7e5c5be3b 100644 --- a/MediaBrowser.Model/Entities/DisplayPreferences.cs +++ b/MediaBrowser.Model/Entities/DisplayPreferences.cs @@ -104,7 +104,7 @@ namespace MediaBrowser.Model.Entities public bool ShowSidebar { get; set; } /// - /// Gets or sets the client + /// Gets or sets the client. /// public string Client { get; set; } } diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 3db72f78a..33c14ec1b 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -13,7 +13,7 @@ using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Model.Entities { /// - /// Class MediaStream + /// Class MediaStream. /// public class MediaStream { diff --git a/MediaBrowser.Model/Entities/MetadataProvider.cs b/MediaBrowser.Model/Entities/MetadataProvider.cs index bcc2b48e7..7fecf67b8 100644 --- a/MediaBrowser.Model/Entities/MetadataProvider.cs +++ b/MediaBrowser.Model/Entities/MetadataProvider.cs @@ -3,28 +3,28 @@ namespace MediaBrowser.Model.Entities { /// - /// Enum MetadataProviders + /// Enum MetadataProviders. /// public enum MetadataProvider { /// - /// The imdb + /// The imdb. /// Imdb = 2, /// - /// The TMDB + /// The TMDB. /// Tmdb = 3, /// - /// The TVDB + /// The TVDB. /// Tvdb = 4, /// - /// The tvcom + /// The tvcom. /// Tvcom = 5, /// - /// Tmdb Collection Id + /// Tmdb Collection Id. /// TmdbCollection = 7, MusicBrainzAlbum = 8, diff --git a/MediaBrowser.Model/Entities/VirtualFolderInfo.cs b/MediaBrowser.Model/Entities/VirtualFolderInfo.cs index 2de02e403..662fa1f78 100644 --- a/MediaBrowser.Model/Entities/VirtualFolderInfo.cs +++ b/MediaBrowser.Model/Entities/VirtualFolderInfo.cs @@ -7,7 +7,7 @@ using MediaBrowser.Model.Configuration; namespace MediaBrowser.Model.Entities { /// - /// Used to hold information about a user's list of configured virtual folders + /// Used to hold information about a user's list of configured virtual folders. /// public class VirtualFolderInfo { diff --git a/MediaBrowser.Model/IO/IZipClient.cs b/MediaBrowser.Model/IO/IZipClient.cs index 83e8a018d..2daa54f22 100644 --- a/MediaBrowser.Model/IO/IZipClient.cs +++ b/MediaBrowser.Model/IO/IZipClient.cs @@ -5,7 +5,7 @@ using System.IO; namespace MediaBrowser.Model.IO { /// - /// Interface IZipClient + /// Interface IZipClient. /// public interface IZipClient { diff --git a/MediaBrowser.Model/LiveTv/ChannelType.cs b/MediaBrowser.Model/LiveTv/ChannelType.cs index b6974cb08..f4c55cb6d 100644 --- a/MediaBrowser.Model/LiveTv/ChannelType.cs +++ b/MediaBrowser.Model/LiveTv/ChannelType.cs @@ -6,7 +6,7 @@ namespace MediaBrowser.Model.LiveTv public enum ChannelType { /// - /// The TV + /// The TV. /// TV, diff --git a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs index d1a94d8b3..3c5c39a16 100644 --- a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs +++ b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs @@ -54,7 +54,7 @@ namespace MediaBrowser.Model.LiveTv public int? StartIndex { get; set; } /// - /// The maximum number of items to return + /// The maximum number of items to return. /// /// The limit. public int? Limit { get; set; } @@ -67,13 +67,13 @@ namespace MediaBrowser.Model.LiveTv public bool EnableUserData { get; set; } /// - /// Used to specific whether to return news or not + /// Used to specific whether to return news or not. /// /// If set to null, all programs will be returned public bool? IsNews { get; set; } /// - /// Used to specific whether to return movies or not + /// Used to specific whether to return movies or not. /// /// If set to null, all programs will be returned public bool? IsMovie { get; set; } @@ -93,7 +93,7 @@ namespace MediaBrowser.Model.LiveTv public string[] SortBy { get; set; } /// - /// The sort order to return results with + /// The sort order to return results with. /// /// The sort order. public SortOrder? SortOrder { get; set; } diff --git a/MediaBrowser.Model/LiveTv/RecordingQuery.cs b/MediaBrowser.Model/LiveTv/RecordingQuery.cs index 264982930..de50adcec 100644 --- a/MediaBrowser.Model/LiveTv/RecordingQuery.cs +++ b/MediaBrowser.Model/LiveTv/RecordingQuery.cs @@ -37,7 +37,7 @@ namespace MediaBrowser.Model.LiveTv public int? StartIndex { get; set; } /// - /// The maximum number of items to return + /// The maximum number of items to return. /// /// The limit. public int? Limit { get; set; } @@ -61,7 +61,7 @@ namespace MediaBrowser.Model.LiveTv public string SeriesTimerId { get; set; } /// - /// Fields to return within the items, in addition to basic information + /// Fields to return within the items, in addition to basic information. /// /// The fields. public ItemFields[] Fields { get; set; } diff --git a/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs b/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs index bda46dd2b..b899a464b 100644 --- a/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs +++ b/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Model.LiveTv public class SeriesTimerQuery { /// - /// Gets or sets the sort by - SortName, Priority + /// Gets or sets the sort by - SortName, Priority. /// /// The sort by. public string? SortBy { get; set; } diff --git a/MediaBrowser.Model/Net/NetworkShare.cs b/MediaBrowser.Model/Net/NetworkShare.cs index a40cf73e4..6344cbe21 100644 --- a/MediaBrowser.Model/Net/NetworkShare.cs +++ b/MediaBrowser.Model/Net/NetworkShare.cs @@ -6,27 +6,27 @@ namespace MediaBrowser.Model.Net public class NetworkShare { /// - /// The name of the computer that this share belongs to + /// The name of the computer that this share belongs to. /// public string Server { get; set; } /// - /// Share name + /// Share name. /// public string Name { get; set; } /// - /// Local path + /// Local path. /// public string Path { get; set; } /// - /// Share type + /// Share type. /// public NetworkShareType ShareType { get; set; } /// - /// Comment + /// Comment. /// public string Remark { get; set; } } diff --git a/MediaBrowser.Model/Notifications/NotificationOption.cs b/MediaBrowser.Model/Notifications/NotificationOption.cs index 144949a3b..ea363d9b1 100644 --- a/MediaBrowser.Model/Notifications/NotificationOption.cs +++ b/MediaBrowser.Model/Notifications/NotificationOption.cs @@ -18,7 +18,7 @@ namespace MediaBrowser.Model.Notifications public string Type { get; set; } /// - /// User Ids to not monitor (it's opt out) + /// User Ids to not monitor (it's opt out). /// public string[] DisabledMonitorUsers { get; set; } diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs index d7cc5ebbe..731d22aaf 100644 --- a/MediaBrowser.Model/Querying/ItemFields.cs +++ b/MediaBrowser.Model/Querying/ItemFields.cs @@ -8,198 +8,198 @@ namespace MediaBrowser.Model.Querying public enum ItemFields { /// - /// The air time + /// The air time. /// AirTime, /// - /// The can delete + /// The can delete. /// CanDelete, /// - /// The can download + /// The can download. /// CanDownload, /// - /// The channel information + /// The channel information. /// ChannelInfo, /// - /// The chapters + /// The chapters. /// Chapters, ChildCount, /// - /// The cumulative run time ticks + /// The cumulative run time ticks. /// CumulativeRunTimeTicks, /// - /// The custom rating + /// The custom rating. /// CustomRating, /// - /// The date created of the item + /// The date created of the item. /// DateCreated, /// - /// The date last media added + /// The date last media added. /// DateLastMediaAdded, /// - /// Item display preferences + /// Item display preferences. /// DisplayPreferencesId, /// - /// The etag + /// The etag. /// Etag, /// - /// The external urls + /// The external urls. /// ExternalUrls, /// - /// Genres + /// Genres. /// Genres, /// - /// The home page URL + /// The home page URL. /// HomePageUrl, /// - /// The item counts + /// The item counts. /// ItemCounts, /// - /// The media source count + /// The media source count. /// MediaSourceCount, /// - /// The media versions + /// The media versions. /// MediaSources, OriginalTitle, /// - /// The item overview + /// The item overview. /// Overview, /// - /// The id of the item's parent + /// The id of the item's parent. /// ParentId, /// - /// The physical path of the item + /// The physical path of the item. /// Path, /// - /// The list of people for the item + /// The list of people for the item. /// People, PlayAccess, /// - /// The production locations + /// The production locations. /// ProductionLocations, /// - /// Imdb, tmdb, etc + /// Imdb, tmdb, etc. /// ProviderIds, /// - /// The aspect ratio of the primary image + /// The aspect ratio of the primary image. /// PrimaryImageAspectRatio, RecursiveItemCount, /// - /// The settings + /// The settings. /// Settings, /// - /// The screenshot image tags + /// The screenshot image tags. /// ScreenshotImageTags, SeriesPrimaryImage, /// - /// The series studio + /// The series studio. /// SeriesStudio, /// - /// The sort name of the item + /// The sort name of the item. /// SortName, /// - /// The special episode numbers + /// The special episode numbers. /// SpecialEpisodeNumbers, /// - /// The studios of the item + /// The studios of the item. /// Studios, BasicSyncInfo, /// - /// The synchronize information + /// The synchronize information. /// SyncInfo, /// - /// The taglines of the item + /// The taglines of the item. /// Taglines, /// - /// The tags + /// The tags. /// Tags, /// - /// The trailer url of the item + /// The trailer url of the item. /// RemoteTrailers, /// - /// The media streams + /// The media streams. /// MediaStreams, /// - /// The season user data + /// The season user data. /// SeasonUserData, /// - /// The service name + /// The service name. /// ServiceName, ThemeSongIds, diff --git a/MediaBrowser.Model/Querying/ItemSortBy.cs b/MediaBrowser.Model/Querying/ItemSortBy.cs index edf71c1a7..0b846bb96 100644 --- a/MediaBrowser.Model/Querying/ItemSortBy.cs +++ b/MediaBrowser.Model/Querying/ItemSortBy.cs @@ -3,7 +3,7 @@ namespace MediaBrowser.Model.Querying { /// - /// These represent sort orders that are known by the core + /// These represent sort orders that are known by the core. /// public static class ItemSortBy { diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs index 0df86cb22..ee13ffc16 100644 --- a/MediaBrowser.Model/Querying/NextUpQuery.cs +++ b/MediaBrowser.Model/Querying/NextUpQuery.cs @@ -33,13 +33,13 @@ namespace MediaBrowser.Model.Querying public int? StartIndex { get; set; } /// - /// The maximum number of items to return + /// The maximum number of items to return. /// /// The limit. public int? Limit { get; set; } /// - /// Fields to return within the items, in addition to basic information + /// Fields to return within the items, in addition to basic information. /// /// The fields. public ItemFields[] Fields { get; set; } diff --git a/MediaBrowser.Model/Querying/QueryResult.cs b/MediaBrowser.Model/Querying/QueryResult.cs index 42586243d..490f48b84 100644 --- a/MediaBrowser.Model/Querying/QueryResult.cs +++ b/MediaBrowser.Model/Querying/QueryResult.cs @@ -15,7 +15,7 @@ namespace MediaBrowser.Model.Querying public IReadOnlyList Items { get; set; } /// - /// The total number of records available + /// The total number of records available. /// /// The total record count. public int TotalRecordCount { get; set; } diff --git a/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs b/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs index ed1aa7ac6..12d537492 100644 --- a/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs +++ b/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs @@ -26,13 +26,13 @@ namespace MediaBrowser.Model.Querying public int? StartIndex { get; set; } /// - /// The maximum number of items to return + /// The maximum number of items to return. /// /// The limit. public int? Limit { get; set; } /// - /// Fields to return within the items, in addition to basic information + /// Fields to return within the items, in addition to basic information. /// /// The fields. public ItemFields[] Fields { get; set; } diff --git a/MediaBrowser.Model/Search/SearchQuery.cs b/MediaBrowser.Model/Search/SearchQuery.cs index 4470f1ad9..c58b13c35 100644 --- a/MediaBrowser.Model/Search/SearchQuery.cs +++ b/MediaBrowser.Model/Search/SearchQuery.cs @@ -8,7 +8,7 @@ namespace MediaBrowser.Model.Search public class SearchQuery { /// - /// The user to localize search results for + /// The user to localize search results for. /// /// The user id. public Guid UserId { get; set; } @@ -26,7 +26,7 @@ namespace MediaBrowser.Model.Search public int? StartIndex { get; set; } /// - /// The maximum number of items to return + /// The maximum number of items to return. /// /// The limit. public int? Limit { get; set; } diff --git a/MediaBrowser.Model/Services/ApiMemberAttribute.cs b/MediaBrowser.Model/Services/ApiMemberAttribute.cs index 7c23eee44..63f3ecd55 100644 --- a/MediaBrowser.Model/Services/ApiMemberAttribute.cs +++ b/MediaBrowser.Model/Services/ApiMemberAttribute.cs @@ -58,7 +58,7 @@ namespace MediaBrowser.Model.Services public string Route { get; set; } /// - /// Whether to exclude this property from being included in the ModelSchema + /// Whether to exclude this property from being included in the ModelSchema. /// public bool ExcludeInSchema { get; set; } } diff --git a/MediaBrowser.Model/Services/IRequest.cs b/MediaBrowser.Model/Services/IRequest.cs index f413f1e17..d8eddf27c 100644 --- a/MediaBrowser.Model/Services/IRequest.cs +++ b/MediaBrowser.Model/Services/IRequest.cs @@ -23,7 +23,7 @@ namespace MediaBrowser.Model.Services string Verb { get; } /// - /// The request ContentType + /// The request ContentType. /// string ContentType { get; } @@ -32,7 +32,7 @@ namespace MediaBrowser.Model.Services string UserAgent { get; } /// - /// The expected Response ContentType for this request + /// The expected Response ContentType for this request. /// string ResponseContentType { get; set; } @@ -55,7 +55,7 @@ namespace MediaBrowser.Model.Services string RemoteIp { get; } /// - /// The value of the Authorization Header used to send the Api Key, null if not available + /// The value of the Authorization Header used to send the Api Key, null if not available. /// string Authorization { get; } @@ -68,7 +68,7 @@ namespace MediaBrowser.Model.Services long ContentLength { get; } /// - /// The value of the Referrer, null if not available + /// The value of the Referrer, null if not available. /// Uri UrlReferrer { get; } } diff --git a/MediaBrowser.Model/Services/IRequiresRequestStream.cs b/MediaBrowser.Model/Services/IRequiresRequestStream.cs index 622626edc..3e5f2da42 100644 --- a/MediaBrowser.Model/Services/IRequiresRequestStream.cs +++ b/MediaBrowser.Model/Services/IRequiresRequestStream.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.Model.Services public interface IRequiresRequestStream { /// - /// The raw Http Request Input Stream + /// The raw Http Request Input Stream. /// Stream RequestStream { get; set; } } diff --git a/MediaBrowser.Model/Session/PlayRequest.cs b/MediaBrowser.Model/Session/PlayRequest.cs index 62b68b49e..66775e735 100644 --- a/MediaBrowser.Model/Session/PlayRequest.cs +++ b/MediaBrowser.Model/Session/PlayRequest.cs @@ -7,7 +7,7 @@ using MediaBrowser.Model.Services; namespace MediaBrowser.Model.Session { /// - /// Class PlayRequest + /// Class PlayRequest. /// public class PlayRequest { @@ -19,7 +19,7 @@ namespace MediaBrowser.Model.Session public Guid[] ItemIds { get; set; } /// - /// Gets or sets the start position ticks that the first item should be played at + /// Gets or sets the start position ticks that the first item should be played at. /// /// The start position ticks. [ApiMember(Name = "StartPositionTicks", Description = "The starting position of the first item.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] diff --git a/MediaBrowser.Model/Sync/SyncCategory.cs b/MediaBrowser.Model/Sync/SyncCategory.cs index 215ac301e..80ad5f56e 100644 --- a/MediaBrowser.Model/Sync/SyncCategory.cs +++ b/MediaBrowser.Model/Sync/SyncCategory.cs @@ -5,15 +5,15 @@ namespace MediaBrowser.Model.Sync public enum SyncCategory { /// - /// The latest + /// The latest. /// Latest = 0, /// - /// The next up + /// The next up. /// NextUp = 1, /// - /// The resume + /// The resume. /// Resume = 2 } diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index a67c38c3a..18ca74ee3 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -23,7 +23,7 @@ namespace MediaBrowser.Model.System }; /// - /// Class SystemInfo + /// Class SystemInfo. /// public class SystemInfo : PublicSystemInfo { diff --git a/MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs b/MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs index c79d7fe75..b08acba2c 100644 --- a/MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs +++ b/MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs @@ -57,7 +57,7 @@ namespace MediaBrowser.Model.Tasks double? CurrentProgress { get; } /// - /// Gets the triggers that define when the task will run + /// Gets the triggers that define when the task will run. /// /// The triggers. /// value diff --git a/MediaBrowser.Model/Tasks/ITaskManager.cs b/MediaBrowser.Model/Tasks/ITaskManager.cs index 4a7f579ec..363773ff7 100644 --- a/MediaBrowser.Model/Tasks/ITaskManager.cs +++ b/MediaBrowser.Model/Tasks/ITaskManager.cs @@ -10,7 +10,7 @@ namespace MediaBrowser.Model.Tasks public interface ITaskManager : IDisposable { /// - /// Gets the list of Scheduled Tasks + /// Gets the list of Scheduled Tasks. /// /// The scheduled tasks. IScheduledTaskWorker[] ScheduledTasks { get; } diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs index 3c94f6215..b16462936 100644 --- a/MediaBrowser.Providers/Manager/ImageSaver.cs +++ b/MediaBrowser.Providers/Manager/ImageSaver.cs @@ -24,19 +24,19 @@ using Season = MediaBrowser.Controller.Entities.TV.Season; namespace MediaBrowser.Providers.Manager { /// - /// Class ImageSaver + /// Class ImageSaver. /// public class ImageSaver { private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); /// - /// The _config + /// The _config. /// private readonly IServerConfigurationManager _config; /// - /// The _directory watchers + /// The _directory watchers. /// private readonly ILibraryMonitor _libraryMonitor; private readonly IFileSystem _fileSystem; diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index 7901503d3..1091c3326 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -168,7 +168,7 @@ namespace MediaBrowser.Providers.Manager } /// - /// Image types that are only one per item + /// Image types that are only one per item. /// private readonly ImageType[] _singularImages = { @@ -189,7 +189,7 @@ namespace MediaBrowser.Providers.Manager } /// - /// Determines if an item already contains the given images + /// Determines if an item already contains the given images. /// /// The item. /// The images. diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 5853c7714..989a4b5d1 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -38,7 +38,7 @@ using Series = MediaBrowser.Controller.Entities.TV.Series; namespace MediaBrowser.Providers.Manager { /// - /// Class ProviderManager + /// Class ProviderManager. /// public class ProviderManager : IProviderManager, IDisposable { diff --git a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs index ba87e0570..8f3df2760 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs @@ -17,7 +17,7 @@ using MediaBrowser.Model.IO; namespace MediaBrowser.Providers.MediaInfo { /// - /// Uses ffmpeg to create video images + /// Uses ffmpeg to create video images. /// public class AudioImageProvider : IDynamicImageProvider { diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs index 73c89e815..de377086a 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs @@ -99,7 +99,7 @@ namespace MediaBrowser.Providers.MediaInfo } /// - /// Fetches data from the tags dictionary + /// Fetches data from the tags dictionary. /// /// The audio. /// The data. diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index ccbe27c1f..29e6d7854 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -342,7 +342,7 @@ namespace MediaBrowser.Providers.MediaInfo } /// - /// Gets information about the longest playlist on a bdrom + /// Gets information about the longest playlist on a bdrom. /// /// The path. /// VideoStream. diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs index 51c26a61c..c0a880bc9 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs @@ -53,7 +53,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search /// The vote_average. public double Vote_Average { get; set; } /// - /// For collection search results + /// For collection search results. /// public string Name { get; set; } /// diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index faeb48b12..3c0922f39 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -25,7 +25,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Plugins.Tmdb.Movies { /// - /// Class MovieDbProvider + /// Class MovieDbProvider. /// public class TmdbMovieProvider : IRemoteMetadataProvider, IHasOrder { @@ -129,7 +129,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies public string Name => TmdbUtils.ProviderName; /// - /// The _TMDB settings task + /// The _TMDB settings task. /// private TmdbSettingsResult _tmdbSettings; diff --git a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs index aabad3ada..26259f1aa 100644 --- a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs +++ b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs @@ -108,7 +108,7 @@ namespace MediaBrowser.Providers.TV /// /// Returns true if a series has any seasons or episodes without season or episode numbers - /// If this data is missing no virtual items will be added in order to prevent possible duplicates + /// If this data is missing no virtual items will be added in order to prevent possible duplicates. /// private bool HasInvalidContent(IList allItems) { @@ -171,7 +171,7 @@ namespace MediaBrowser.Providers.TV } /// - /// Removes the virtual entry after a corresponding physical version has been added + /// Removes the virtual entry after a corresponding physical version has been added. /// private bool RemoveObsoleteOrMissingEpisodes( IEnumerable allRecursiveChildren, diff --git a/RSSDP/DiscoveredSsdpDevice.cs b/RSSDP/DiscoveredSsdpDevice.cs index 1244ce523..d7561fc93 100644 --- a/RSSDP/DiscoveredSsdpDevice.cs +++ b/RSSDP/DiscoveredSsdpDevice.cs @@ -55,7 +55,7 @@ namespace Rssdp } /// - /// Returns the headers from the SSDP device response message + /// Returns the headers from the SSDP device response message. /// public HttpHeaders ResponseHeaders { get; set; } -- cgit v1.2.3 From 64fb173dad77a38273548434bee683b85e323345 Mon Sep 17 00:00:00 2001 From: David Date: Sat, 20 Jun 2020 15:59:41 +0200 Subject: Move DashboardController to Jellyfin.Api --- Emby.Server.Implementations/ApplicationHost.cs | 4 - .../Emby.Server.Implementations.csproj | 1 - Jellyfin.Api/Controllers/DashboardController.cs | 264 ++++++++++++++++ Jellyfin.Api/Models/ConfigurationPageInfo.cs | 85 ++++++ Jellyfin.Server/Program.cs | 4 +- .../Api/ConfigurationPageInfo.cs | 63 ---- MediaBrowser.WebDashboard/Api/DashboardService.cs | 340 --------------------- .../MediaBrowser.WebDashboard.csproj | 42 --- .../Properties/AssemblyInfo.cs | 21 -- MediaBrowser.WebDashboard/ServerEntryPoint.cs | 42 --- MediaBrowser.sln | 6 - 11 files changed, 351 insertions(+), 521 deletions(-) create mode 100644 Jellyfin.Api/Controllers/DashboardController.cs create mode 100644 Jellyfin.Api/Models/ConfigurationPageInfo.cs delete mode 100644 MediaBrowser.WebDashboard/Api/ConfigurationPageInfo.cs delete mode 100644 MediaBrowser.WebDashboard/Api/DashboardService.cs delete mode 100644 MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj delete mode 100644 MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs delete mode 100644 MediaBrowser.WebDashboard/ServerEntryPoint.cs (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 5772dd479..25ee7e9ec 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -97,7 +97,6 @@ using MediaBrowser.Providers.Chapters; using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.TheTvdb; using MediaBrowser.Providers.Subtitles; -using MediaBrowser.WebDashboard.Api; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -1037,9 +1036,6 @@ namespace Emby.Server.Implementations // Include composable parts in the Api assembly yield return typeof(ApiEntryPoint).Assembly; - // Include composable parts in the Dashboard assembly - yield return typeof(DashboardService).Assembly; - // Include composable parts in the Model assembly yield return typeof(SystemInfo).Assembly; diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index e71e437ac..5272e2692 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -13,7 +13,6 @@ - diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs new file mode 100644 index 000000000..6a7bf7d0a --- /dev/null +++ b/Jellyfin.Api/Controllers/DashboardController.cs @@ -0,0 +1,264 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using Jellyfin.Api.Models; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Plugins; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; + +namespace Jellyfin.Api.Controllers +{ + /// + /// The dashboard controller. + /// + public class DashboardController : BaseJellyfinApiController + { + private readonly IServerApplicationHost _appHost; + private readonly IConfiguration _appConfig; + private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly IResourceFileManager _resourceFileManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + public DashboardController( + IServerApplicationHost appHost, + IConfiguration appConfig, + IResourceFileManager resourceFileManager, + IServerConfigurationManager serverConfigurationManager) + { + _appHost = appHost; + _appConfig = appConfig; + _resourceFileManager = resourceFileManager; + _serverConfigurationManager = serverConfigurationManager; + } + + /// + /// Gets the path of the directory containing the static web interface content, or null if the server is not + /// hosting the web client. + /// + private string? WebClientUiPath => GetWebClientUiPath(_appConfig, _serverConfigurationManager); + + /// + /// Gets the configuration pages. + /// + /// Whether to enable in the main menu. + /// The . + /// ConfigurationPages returned. + /// Server still loading. + /// An with infos about the plugins. + [HttpGet("/web/ConfigurationPages")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult> GetConfigurationPages( + [FromQuery] bool? enableInMainMenu, + [FromQuery] ConfigurationPageType? pageType) + { + const string unavailableMessage = "The server is still loading. Please try again momentarily."; + + var pages = _appHost.GetExports().ToList(); + + if (pages == null) + { + return NotFound(unavailableMessage); + } + + // Don't allow a failing plugin to fail them all + var configPages = pages.Select(p => + { + return new ConfigurationPageInfo(p); + }) + .Where(i => i != null) + .ToList(); + + configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages)); + + if (pageType != null) + { + configPages = configPages.Where(p => p.ConfigurationPageType == pageType).ToList(); + } + + if (enableInMainMenu.HasValue) + { + configPages = configPages.Where(p => p.EnableInMainMenu == enableInMainMenu.Value).ToList(); + } + + return configPages; + } + + /// + /// Gets a dashboard configuration page. + /// + /// The name of the page. + /// ConfigurationPage returned. + /// Plugin configuration page not found. + /// The configuration page. + [HttpGet("/web/ConfigurationPage")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GetDashboardConfigurationPage([FromQuery] string name) + { + IPlugin? plugin = null; + Stream? stream = null; + + var isJs = false; + var isTemplate = false; + + var page = _appHost.GetExports().FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase)); + if (page != null) + { + plugin = page.Plugin; + stream = page.GetHtmlStream(); + } + + if (plugin == null) + { + var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, name, StringComparison.OrdinalIgnoreCase)); + if (altPage != null) + { + plugin = altPage.Item2; + stream = plugin.GetType().Assembly.GetManifestResourceStream(altPage.Item1.EmbeddedResourcePath); + + isJs = string.Equals(Path.GetExtension(altPage.Item1.EmbeddedResourcePath), ".js", StringComparison.OrdinalIgnoreCase); + isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html", StringComparison.Ordinal); + } + } + + if (plugin != null && stream != null) + { + if (isJs) + { + return File(stream, MimeTypes.GetMimeType("page.js")); + } + + if (isTemplate) + { + return File(stream, MimeTypes.GetMimeType("page.html")); + } + + return File(stream, MimeTypes.GetMimeType("page.html")); + } + + return NotFound(); + } + + /// + /// Gets the robots.txt. + /// + /// Robots.txt returned. + /// The robots.txt. + [HttpGet("/robots.txt")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ApiExplorerSettings(IgnoreApi = true)] + public ActionResult GetRobotsTxt() + { + return GetWebClientResource("robots.txt", string.Empty); + } + + /// + /// Gets a resource from the web client. + /// + /// The resource name. + /// The v. + /// Web client returned. + /// Server does not host a web client. + /// The resource. + [HttpGet("/web/{*resourceName}")] + [ApiExplorerSettings(IgnoreApi = true)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "v", Justification = "Imported from ServiceStack")] + public ActionResult GetWebClientResource( + [FromRoute] string resourceName, + [FromQuery] string? v) + { + if (!_appConfig.HostWebClient() || WebClientUiPath == null) + { + return NotFound("Server does not host a web client."); + } + + var path = resourceName; + var basePath = WebClientUiPath; + + // Bounce them to the startup wizard if it hasn't been completed yet + if (!_serverConfigurationManager.Configuration.IsStartupWizardCompleted + && !Request.Path.Value.Contains("wizard", StringComparison.OrdinalIgnoreCase) + && Request.Path.Value.Contains("index", StringComparison.OrdinalIgnoreCase)) + { + return Redirect("index.html?start=wizard#!/wizardstart.html"); + } + + var stream = new FileStream(_resourceFileManager.GetResourcePath(basePath, path), FileMode.Open, FileAccess.Read); + return File(stream, MimeTypes.GetMimeType(path)); + } + + /// + /// Gets the favicon. + /// + /// Favicon.ico returned. + /// The favicon. + [HttpGet("/favicon.ico")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ApiExplorerSettings(IgnoreApi = true)] + public ActionResult GetFavIcon() + { + return GetWebClientResource("favicon.ico", string.Empty); + } + + /// + /// Gets the path of the directory containing the static web interface content. + /// + /// The app configuration. + /// The server configuration manager. + /// The directory path, or null if the server is not hosting the web client. + public static string? GetWebClientUiPath(IConfiguration appConfig, IServerConfigurationManager serverConfigManager) + { + if (!appConfig.HostWebClient()) + { + return null; + } + + if (!string.IsNullOrEmpty(serverConfigManager.Configuration.DashboardSourcePath)) + { + return serverConfigManager.Configuration.DashboardSourcePath; + } + + return serverConfigManager.ApplicationPaths.WebPath; + } + + private IEnumerable GetConfigPages(IPlugin plugin) + { + return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1)); + } + + private IEnumerable> GetPluginPages(IPlugin plugin) + { + var hasConfig = plugin as IHasWebPages; + + if (hasConfig == null) + { + return new List>(); + } + + return hasConfig.GetPages().Select(i => new Tuple(i, plugin)); + } + + private IEnumerable> GetPluginPages() + { + return _appHost.Plugins.SelectMany(GetPluginPages); + } + } +} diff --git a/Jellyfin.Api/Models/ConfigurationPageInfo.cs b/Jellyfin.Api/Models/ConfigurationPageInfo.cs new file mode 100644 index 000000000..2aa6373aa --- /dev/null +++ b/Jellyfin.Api/Models/ConfigurationPageInfo.cs @@ -0,0 +1,85 @@ +using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Model.Plugins; + +namespace Jellyfin.Api.Models +{ + /// + /// The configuration page info. + /// + public class ConfigurationPageInfo + { + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + public ConfigurationPageInfo(IPluginConfigurationPage page) + { + Name = page.Name; + + ConfigurationPageType = page.ConfigurationPageType; + + if (page.Plugin != null) + { + DisplayName = page.Plugin.Name; + // Don't use "N" because it needs to match Plugin.Id + PluginId = page.Plugin.Id.ToString(); + } + } + + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + /// Instance of interface. + public ConfigurationPageInfo(IPlugin plugin, PluginPageInfo page) + { + Name = page.Name; + EnableInMainMenu = page.EnableInMainMenu; + MenuSection = page.MenuSection; + MenuIcon = page.MenuIcon; + DisplayName = string.IsNullOrWhiteSpace(page.DisplayName) ? plugin.Name : page.DisplayName; + + // Don't use "N" because it needs to match Plugin.Id + PluginId = plugin.Id.ToString(); + } + + /// + /// Gets or sets the name. + /// + /// The name. + public string Name { get; set; } + + /// + /// Gets or sets a value indicating whether the configurations page is enabled in the main menu. + /// + public bool EnableInMainMenu { get; set; } + + /// + /// Gets or sets the menu section. + /// + public string? MenuSection { get; set; } + + /// + /// Gets or sets the menu icon. + /// + public string? MenuIcon { get; set; } + + /// + /// Gets or sets the display name. + /// + public string? DisplayName { get; set; } + + /// + /// Gets or sets the type of the configuration page. + /// + /// The type of the configuration page. + public ConfigurationPageType ConfigurationPageType { get; set; } + + /// + /// Gets or sets the plugin id. + /// + /// The plugin id. + public string? PluginId { get; set; } + } +} diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 3971a08e9..dfc7bbbb1 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -14,9 +14,9 @@ using Emby.Server.Implementations; using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Networking; +using Jellyfin.Api.Controllers; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Extensions; -using MediaBrowser.WebDashboard.Api; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; @@ -172,7 +172,7 @@ namespace Jellyfin.Server // If hosting the web client, validate the client content path if (startupConfig.HostWebClient()) { - string webContentPath = DashboardService.GetDashboardUIPath(startupConfig, appHost.ServerConfigurationManager); + string? webContentPath = DashboardController.GetWebClientUiPath(startupConfig, appHost.ServerConfigurationManager); if (!Directory.Exists(webContentPath) || Directory.GetFiles(webContentPath).Length == 0) { throw new InvalidOperationException( diff --git a/MediaBrowser.WebDashboard/Api/ConfigurationPageInfo.cs b/MediaBrowser.WebDashboard/Api/ConfigurationPageInfo.cs deleted file mode 100644 index e49a4be8a..000000000 --- a/MediaBrowser.WebDashboard/Api/ConfigurationPageInfo.cs +++ /dev/null @@ -1,63 +0,0 @@ -#pragma warning disable CS1591 - -using MediaBrowser.Common.Plugins; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Model.Plugins; - -namespace MediaBrowser.WebDashboard.Api -{ - public class ConfigurationPageInfo - { - public ConfigurationPageInfo(IPluginConfigurationPage page) - { - Name = page.Name; - - ConfigurationPageType = page.ConfigurationPageType; - - if (page.Plugin != null) - { - DisplayName = page.Plugin.Name; - // Don't use "N" because it needs to match Plugin.Id - PluginId = page.Plugin.Id.ToString(); - } - } - - public ConfigurationPageInfo(IPlugin plugin, PluginPageInfo page) - { - Name = page.Name; - EnableInMainMenu = page.EnableInMainMenu; - MenuSection = page.MenuSection; - MenuIcon = page.MenuIcon; - DisplayName = string.IsNullOrWhiteSpace(page.DisplayName) ? plugin.Name : page.DisplayName; - - // Don't use "N" because it needs to match Plugin.Id - PluginId = plugin.Id.ToString(); - } - - /// - /// Gets or sets the name. - /// - /// The name. - public string Name { get; set; } - - public bool EnableInMainMenu { get; set; } - - public string MenuSection { get; set; } - - public string MenuIcon { get; set; } - - public string DisplayName { get; set; } - - /// - /// Gets or sets the type of the configuration page. - /// - /// The type of the configuration page. - public ConfigurationPageType ConfigurationPageType { get; set; } - - /// - /// Gets or sets the plugin id. - /// - /// The plugin id. - public string PluginId { get; set; } - } -} diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs deleted file mode 100644 index 63cbfd9e4..000000000 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ /dev/null @@ -1,340 +0,0 @@ -#pragma warning disable CS1591 -#pragma warning disable SA1402 -#pragma warning disable SA1649 - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Plugins; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Extensions; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Plugins; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.WebDashboard.Api -{ - /// - /// Class GetDashboardConfigurationPages. - /// - [Route("/web/ConfigurationPages", "GET")] - public class GetDashboardConfigurationPages : IReturn> - { - /// - /// Gets or sets the type of the page. - /// - /// The type of the page. - public ConfigurationPageType? PageType { get; set; } - - public bool? EnableInMainMenu { get; set; } - } - - /// - /// Class GetDashboardConfigurationPage. - /// - [Route("/web/ConfigurationPage", "GET")] - public class GetDashboardConfigurationPage - { - /// - /// Gets or sets the name. - /// - /// The name. - public string Name { get; set; } - } - - [Route("/robots.txt", "GET", IsHidden = true)] - public class GetRobotsTxt - { - } - - /// - /// Class GetDashboardResource. - /// - [Route("/web/{ResourceName*}", "GET", IsHidden = true)] - public class GetDashboardResource - { - /// - /// Gets or sets the name. - /// - /// The name. - public string ResourceName { get; set; } - - /// - /// Gets or sets the V. - /// - /// The V. - public string V { get; set; } - } - - [Route("/favicon.ico", "GET", IsHidden = true)] - public class GetFavIcon - { - } - - /// - /// Class DashboardService. - /// - public class DashboardService : IService, IRequiresRequest - { - /// - /// Gets or sets the logger. - /// - /// The logger. - private readonly ILogger _logger; - - /// - /// Gets or sets the HTTP result factory. - /// - /// The HTTP result factory. - private readonly IHttpResultFactory _resultFactory; - private readonly IServerApplicationHost _appHost; - private readonly IConfiguration _appConfig; - private readonly IServerConfigurationManager _serverConfigurationManager; - private readonly IFileSystem _fileSystem; - private readonly IResourceFileManager _resourceFileManager; - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - /// The application host. - /// The application configuration. - /// The resource file manager. - /// The server configuration manager. - /// The file system. - /// The result factory. - public DashboardService( - ILogger logger, - IServerApplicationHost appHost, - IConfiguration appConfig, - IResourceFileManager resourceFileManager, - IServerConfigurationManager serverConfigurationManager, - IFileSystem fileSystem, - IHttpResultFactory resultFactory) - { - _logger = logger; - _appHost = appHost; - _appConfig = appConfig; - _resourceFileManager = resourceFileManager; - _serverConfigurationManager = serverConfigurationManager; - _fileSystem = fileSystem; - _resultFactory = resultFactory; - } - - /// - /// Gets or sets the request context. - /// - /// The request context. - public IRequest Request { get; set; } - - /// - /// Gets the path of the directory containing the static web interface content, or null if the server is not - /// hosting the web client. - /// - public string DashboardUIPath => GetDashboardUIPath(_appConfig, _serverConfigurationManager); - - /// - /// Gets the path of the directory containing the static web interface content. - /// - /// The app configuration. - /// The server configuration manager. - /// The directory path, or null if the server is not hosting the web client. - public static string GetDashboardUIPath(IConfiguration appConfig, IServerConfigurationManager serverConfigManager) - { - if (!appConfig.HostWebClient()) - { - return null; - } - - if (!string.IsNullOrEmpty(serverConfigManager.Configuration.DashboardSourcePath)) - { - return serverConfigManager.Configuration.DashboardSourcePath; - } - - return serverConfigManager.ApplicationPaths.WebPath; - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Get(GetFavIcon request) - { - return Get(new GetDashboardResource - { - ResourceName = "favicon.ico" - }); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public Task Get(GetDashboardConfigurationPage request) - { - IPlugin plugin = null; - Stream stream = null; - - var isJs = false; - var isTemplate = false; - - var page = ServerEntryPoint.Instance.PluginConfigurationPages.FirstOrDefault(p => string.Equals(p.Name, request.Name, StringComparison.OrdinalIgnoreCase)); - if (page != null) - { - plugin = page.Plugin; - stream = page.GetHtmlStream(); - } - - if (plugin == null) - { - var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, request.Name, StringComparison.OrdinalIgnoreCase)); - if (altPage != null) - { - plugin = altPage.Item2; - stream = plugin.GetType().Assembly.GetManifestResourceStream(altPage.Item1.EmbeddedResourcePath); - - isJs = string.Equals(Path.GetExtension(altPage.Item1.EmbeddedResourcePath), ".js", StringComparison.OrdinalIgnoreCase); - isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html", StringComparison.Ordinal); - } - } - - if (plugin != null && stream != null) - { - if (isJs) - { - return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.js"), () => Task.FromResult(stream)); - } - - if (isTemplate) - { - return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => Task.FromResult(stream)); - } - - return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => Task.FromResult(stream)); - } - - throw new ResourceNotFoundException(); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetDashboardConfigurationPages request) - { - const string unavailableMessage = "The server is still loading. Please try again momentarily."; - - var instance = ServerEntryPoint.Instance; - - if (instance == null) - { - throw new InvalidOperationException(unavailableMessage); - } - - var pages = instance.PluginConfigurationPages; - - if (pages == null) - { - throw new InvalidOperationException(unavailableMessage); - } - - // Don't allow a failing plugin to fail them all - var configPages = pages.Select(p => - { - try - { - return new ConfigurationPageInfo(p); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting plugin information from {Plugin}", p.GetType().Name); - return null; - } - }) - .Where(i => i != null) - .ToList(); - - configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages)); - - if (request.PageType.HasValue) - { - configPages = configPages.Where(p => p.ConfigurationPageType == request.PageType.Value).ToList(); - } - - if (request.EnableInMainMenu.HasValue) - { - configPages = configPages.Where(p => p.EnableInMainMenu == request.EnableInMainMenu.Value).ToList(); - } - - return configPages; - } - - private IEnumerable> GetPluginPages() - { - return _appHost.Plugins.SelectMany(GetPluginPages); - } - - private IEnumerable> GetPluginPages(IPlugin plugin) - { - var hasConfig = plugin as IHasWebPages; - - if (hasConfig == null) - { - return new List>(); - } - - return hasConfig.GetPages().Select(i => new Tuple(i, plugin)); - } - - private IEnumerable GetConfigPages(IPlugin plugin) - { - return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1)); - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Get(GetRobotsTxt request) - { - return Get(new GetDashboardResource - { - ResourceName = "robots.txt" - }); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public async Task Get(GetDashboardResource request) - { - if (!_appConfig.HostWebClient() || DashboardUIPath == null) - { - throw new ResourceNotFoundException(); - } - - var path = request?.ResourceName; - var basePath = DashboardUIPath; - - // Bounce them to the startup wizard if it hasn't been completed yet - if (!_serverConfigurationManager.Configuration.IsStartupWizardCompleted - && !Request.RawUrl.Contains("wizard", StringComparison.OrdinalIgnoreCase) - && Request.RawUrl.Contains("index", StringComparison.OrdinalIgnoreCase)) - { - Request.Response.Redirect("index.html?start=wizard#!/wizardstart.html"); - return null; - } - - return await _resultFactory.GetStaticFileResult(Request, _resourceFileManager.GetResourcePath(basePath, path)).ConfigureAwait(false); - } - } -} diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj deleted file mode 100644 index bcaee50f2..000000000 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ /dev/null @@ -1,42 +0,0 @@ - - - - - {5624B7B5-B5A7-41D8-9F10-CC5611109619} - - - - - - - - - - - - - - PreserveNewest - - - - - netstandard2.1 - false - true - true - - - - - - - - - - - - ../jellyfin.ruleset - - - diff --git a/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs b/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs deleted file mode 100644 index 584d49021..000000000 --- a/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Reflection; -using System.Resources; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("MediaBrowser.WebDashboard")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: NeutralResourcesLanguage("en")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] diff --git a/MediaBrowser.WebDashboard/ServerEntryPoint.cs b/MediaBrowser.WebDashboard/ServerEntryPoint.cs deleted file mode 100644 index 5c7e8b3c7..000000000 --- a/MediaBrowser.WebDashboard/ServerEntryPoint.cs +++ /dev/null @@ -1,42 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using MediaBrowser.Common; -using MediaBrowser.Controller.Plugins; - -namespace MediaBrowser.WebDashboard -{ - public sealed class ServerEntryPoint : IServerEntryPoint - { - private readonly IApplicationHost _appHost; - - public ServerEntryPoint(IApplicationHost appHost) - { - _appHost = appHost; - Instance = this; - } - - public static ServerEntryPoint Instance { get; private set; } - - /// - /// Gets the list of plugin configuration pages. - /// - /// The configuration pages. - public List PluginConfigurationPages { get; private set; } - - /// - public Task RunAsync() - { - PluginConfigurationPages = _appHost.GetExports().ToList(); - - return Task.CompletedTask; - } - - /// - public void Dispose() - { - } - } -} diff --git a/MediaBrowser.sln b/MediaBrowser.sln index e100c0b1c..0362eff1c 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -12,8 +12,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Common", "Medi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Model", "MediaBrowser.Model\MediaBrowser.Model.csproj", "{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.WebDashboard", "MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj", "{5624B7B5-B5A7-41D8-9F10-CC5611109619}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Providers", "MediaBrowser.Providers\MediaBrowser.Providers.csproj", "{442B5058-DCAF-4263-BB6A-F21E31120A1B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.XbmcMetadata", "MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj", "{23499896-B135-4527-8574-C26E926EA99E}" @@ -94,10 +92,6 @@ Global {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|Any CPU.Build.0 = Debug|Any CPU {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.ActiveCfg = Release|Any CPU {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.Build.0 = Release|Any CPU - {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.Build.0 = Release|Any CPU {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|Any CPU.Build.0 = Debug|Any CPU {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release|Any CPU.ActiveCfg = Release|Any CPU -- cgit v1.2.3 From 438977350892179d5dc2259316b0fe27ceb1e5da Mon Sep 17 00:00:00 2001 From: Neil Burrows Date: Mon, 29 Jun 2020 17:08:20 +0100 Subject: Respect FFMpeg path passed via Environment Variable --- Emby.Server.Implementations/ApplicationHost.cs | 3 +-- Jellyfin.Server/StartupOptions.cs | 5 +++++ MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs | 5 +++++ MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 7 ++++--- 4 files changed, 15 insertions(+), 5 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 14267b561..8d213ac57 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -568,8 +568,7 @@ namespace Emby.Server.Implementations // TODO: Refactor to eliminate the circular dependency here so that Lazy isn't required // TODO: Add StartupOptions.FFmpegPath to IConfiguration and remove this custom activation serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); - serviceCollection.AddSingleton(provider => - ActivatorUtilities.CreateInstance(provider, _startupOptions.FFmpegPath ?? string.Empty)); + serviceCollection.AddSingleton(); // TODO: Refactor to eliminate the circular dependencies here so that Lazy isn't required serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index a26114e77..41a1430d2 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -101,6 +101,11 @@ namespace Jellyfin.Server config.Add(UdpServer.AddressOverrideConfigKey, PublishedServerUrl.ToString()); } + if (FFmpegPath != null) + { + config.Add(ConfigurationExtensions.FfmpegPathKey, FFmpegPath); + } + return config; } } diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs index c0043c0ef..c2932cc4c 100644 --- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs +++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs @@ -23,6 +23,11 @@ namespace MediaBrowser.Controller.Extensions /// public const string FfmpegAnalyzeDurationKey = "FFmpeg:analyzeduration"; + /// + /// The key for the FFmpeg path option. + /// + public const string FfmpegPathKey = "ffmpeg"; + /// /// The key for a setting that indicates whether playlists should allow duplicate entries. /// diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 9397a347f..af99f2521 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -21,6 +21,7 @@ using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; using System.Diagnostics; +using Microsoft.Extensions.Configuration; namespace MediaBrowser.MediaEncoding.Encoder { @@ -46,7 +47,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private readonly object _runningProcessesLock = new object(); private readonly List _runningProcesses = new List(); - private string _ffmpegPath; + private string _ffmpegPath = string.Empty; private string _ffprobePath; public MediaEncoder( @@ -55,14 +56,14 @@ namespace MediaBrowser.MediaEncoding.Encoder IFileSystem fileSystem, ILocalizationManager localization, Lazy encodingHelperFactory, - string startupOptionsFFmpegPath) + IConfiguration config) { _logger = logger; _configurationManager = configurationManager; _fileSystem = fileSystem; _localization = localization; _encodingHelperFactory = encodingHelperFactory; - _startupOptionFFmpegPath = startupOptionsFFmpegPath; + _startupOptionFFmpegPath = config.GetValue(Controller.Extensions.ConfigurationExtensions.FfmpegPathKey) ?? ""; } private EncodingHelper EncodingHelper => _encodingHelperFactory.Value; -- cgit v1.2.3 From 4748df26b6b690f58867dbc2c39f23c44553f7a5 Mon Sep 17 00:00:00 2001 From: Neil Burrows Date: Mon, 29 Jun 2020 17:25:12 +0100 Subject: Remove un-needed comment --- Emby.Server.Implementations/ApplicationHost.cs | 1 - 1 file changed, 1 deletion(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 8d213ac57..18b6834ef 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -566,7 +566,6 @@ namespace Emby.Server.Implementations serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); // TODO: Refactor to eliminate the circular dependency here so that Lazy isn't required - // TODO: Add StartupOptions.FFmpegPath to IConfiguration and remove this custom activation serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); serviceCollection.AddSingleton(); -- cgit v1.2.3 From ae6eaa7f02bcd7faf30613f1472e16d03f5637d5 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sat, 4 Jul 2020 22:06:27 +0200 Subject: Minor fixes --- Emby.Server.Implementations/ApplicationHost.cs | 26 +++++++++------------- Jellyfin.Server/Program.cs | 4 ++-- MediaBrowser.Model/Dlna/StreamInfo.cs | 2 +- .../Plugins/Tmdb/Movies/TmdbMovieProvider.cs | 12 +++++----- 4 files changed, 20 insertions(+), 24 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 18b6834ef..531775302 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -43,9 +43,9 @@ using Emby.Server.Implementations.Security; using Emby.Server.Implementations.Serialization; using Emby.Server.Implementations.Services; using Emby.Server.Implementations.Session; +using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; -using Emby.Server.Implementations.SyncPlay; using MediaBrowser.Api; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; @@ -78,8 +78,8 @@ using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Subtitles; -using MediaBrowser.Controller.TV; using MediaBrowser.Controller.SyncPlay; +using MediaBrowser.Controller.TV; using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.Model.Configuration; @@ -484,12 +484,10 @@ namespace Emby.Server.Implementations foreach (var plugin in Plugins) { - pluginBuilder.AppendLine( - string.Format( - CultureInfo.InvariantCulture, - "{0} {1}", - plugin.Name, - plugin.Version)); + pluginBuilder.Append(plugin.Name) + .Append(' ') + .Append(plugin.Version) + .AppendLine(); } Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString()); @@ -1153,7 +1151,7 @@ namespace Emby.Server.Implementations return null; } - return GetLocalApiUrl(addresses.First()); + return GetLocalApiUrl(addresses[0]); } catch (Exception ex) { @@ -1226,7 +1224,7 @@ namespace Emby.Server.Implementations var addresses = ServerConfigurationManager .Configuration .LocalNetworkAddresses - .Select(NormalizeConfiguredLocalAddress) + .Select(x => NormalizeConfiguredLocalAddress(x)) .Where(i => i != null) .ToList(); @@ -1247,8 +1245,7 @@ namespace Emby.Server.Implementations } } - var valid = await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false); - if (valid) + if (await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false)) { resultList.Add(address); @@ -1262,13 +1259,12 @@ namespace Emby.Server.Implementations return resultList; } - public IPAddress NormalizeConfiguredLocalAddress(string address) + public IPAddress NormalizeConfiguredLocalAddress(ReadOnlySpan address) { var index = address.Trim('/').IndexOf('/'); - if (index != -1) { - address = address.Substring(index + 1); + address = address.Slice(index + 1); } if (IPAddress.TryParse(address.Trim('/'), out IPAddress result)) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 4d898ff5e..ef3ebe90c 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -274,10 +274,10 @@ namespace Jellyfin.Server var addresses = appHost.ServerConfigurationManager .Configuration .LocalNetworkAddresses - .Select(appHost.NormalizeConfiguredLocalAddress) + .Select(x => appHost.NormalizeConfiguredLocalAddress(x)) .Where(i => i != null) .ToHashSet(); - if (addresses.Any() && !addresses.Contains(IPAddress.Any)) + if (addresses.Count > 0 && !addresses.Contains(IPAddress.Any)) { if (!addresses.Contains(IPAddress.Loopback)) { diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index b89e9ce90..fded0d6ed 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -144,7 +144,7 @@ namespace MediaBrowser.Model.Dlna public Dictionary StreamOptions { get; private set; } - public string MediaSourceId => MediaSource == null ? null : MediaSource.Id; + public string MediaSourceId => MediaSource?.Id; public bool IsDirectStream => PlayMethod == PlayMethod.DirectStream || diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index 5e2b83294..df471896f 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -148,7 +148,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies using (HttpResponseInfo response = await GetMovieDbResponse(new HttpRequestOptions { - Url = string.Format(TmdbConfigUrl, TmdbUtils.ApiKey), + Url = string.Format(CultureInfo.InvariantCulture, TmdbConfigUrl, TmdbUtils.ApiKey), CancellationToken = cancellationToken, AcceptHeader = TmdbUtils.AcceptHeader }).ConfigureAwait(false)) @@ -245,7 +245,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies preferredLanguage = "alllang"; } - var filename = string.Format("all-{0}.json", preferredLanguage); + var filename = string.Format(CultureInfo.InvariantCulture, "all-{0}.json", preferredLanguage); return Path.Combine(path, filename); } @@ -276,7 +276,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies languages.Add("en"); } - return string.Join(",", languages.ToArray()); + return string.Join(",", languages); } public static string NormalizeLanguage(string language) @@ -321,11 +321,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies /// Task{CompleteMovieData}. internal async Task FetchMainResult(string id, bool isTmdbId, string language, CancellationToken cancellationToken) { - var url = string.Format(GetMovieInfo3, id, TmdbUtils.ApiKey); + var url = string.Format(CultureInfo.InvariantCulture, GetMovieInfo3, id, TmdbUtils.ApiKey); if (!string.IsNullOrEmpty(language)) { - url += string.Format("&language={0}", NormalizeLanguage(language)); + url += string.Format(CultureInfo.InvariantCulture, "&language={0}", NormalizeLanguage(language)); // Get images in english and with no language url += "&include_image_language=" + GetImageLanguagesParam(language); @@ -377,7 +377,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies { _logger.LogInformation("MovieDbProvider couldn't find meta for language " + language + ". Trying English..."); - url = string.Format(GetMovieInfo3, id, TmdbUtils.ApiKey) + "&language=en"; + url = string.Format(CultureInfo.InvariantCulture,GetMovieInfo3, id, TmdbUtils.ApiKey) + "&language=en"; if (!string.IsNullOrEmpty(language)) { -- cgit v1.2.3 From 942c733d4f13046cedd19fa849eec88b13d2d2c7 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sun, 5 Jul 2020 15:36:29 -0400 Subject: Fix TypeLoadException during plugin load --- Emby.Server.Implementations/ApplicationHost.cs | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 18b6834ef..fe1961b70 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -871,6 +871,11 @@ namespace Emby.Server.Implementations Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName); continue; } + catch (TypeLoadException ex) + { + Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName); + continue; + } foreach (Type type in exportedTypes) { -- cgit v1.2.3 From 3b085f6a03bfe945e12b104bb042be1d00981cd2 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Wed, 17 Jun 2020 10:24:25 -0400 Subject: Remove UserManager.AddParts --- Emby.Server.Implementations/ApplicationHost.cs | 4 ++- .../Users/UserManager.cs | 35 +++++++++++----------- MediaBrowser.Controller/Library/IUserManager.cs | 3 -- 3 files changed, 20 insertions(+), 22 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index f6077400d..908fe4b30 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -547,6 +547,9 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(ServerConfigurationManager); + serviceCollection.AddSingleton>>(() => GetExports()); + serviceCollection.AddSingleton>>(() => GetExports()); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -795,7 +798,6 @@ namespace Emby.Server.Implementations Resolve().AddParts(GetExports()); Resolve().AddParts(GetExports(), GetExports()); - Resolve().AddParts(GetExports(), GetExports()); Resolve().AddParts(GetExports()); } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 6283a1bca..d4b91f3c5 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -39,12 +39,11 @@ namespace Jellyfin.Server.Implementations.Users private readonly IApplicationHost _appHost; private readonly IImageProcessor _imageProcessor; private readonly ILogger _logger; - - private IAuthenticationProvider[] _authenticationProviders = null!; - private DefaultAuthenticationProvider _defaultAuthenticationProvider = null!; - private InvalidAuthProvider _invalidAuthProvider = null!; - private IPasswordResetProvider[] _passwordResetProviders = null!; - private DefaultPasswordResetProvider _defaultPasswordResetProvider = null!; + private readonly IReadOnlyCollection _passwordResetProviders; + private readonly IReadOnlyCollection _authenticationProviders; + private readonly InvalidAuthProvider _invalidAuthProvider; + private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider; + private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider; /// /// Initializes a new instance of the class. @@ -55,13 +54,17 @@ namespace Jellyfin.Server.Implementations.Users /// The application host. /// The image processor. /// The logger. + /// A function that returns available password reset providers. + /// A function that returns available authentication providers. public UserManager( JellyfinDbProvider dbProvider, ICryptoProvider cryptoProvider, INetworkManager networkManager, IApplicationHost appHost, IImageProcessor imageProcessor, - ILogger logger) + ILogger logger, + Func> passwordResetProviders, + Func> authenticationProviders) { _dbProvider = dbProvider; _cryptoProvider = cryptoProvider; @@ -69,6 +72,13 @@ namespace Jellyfin.Server.Implementations.Users _appHost = appHost; _imageProcessor = imageProcessor; _logger = logger; + + _passwordResetProviders = passwordResetProviders.Invoke(); + _authenticationProviders = authenticationProviders.Invoke(); + + _invalidAuthProvider = _authenticationProviders.OfType().First(); + _defaultAuthenticationProvider = _authenticationProviders.OfType().First(); + _defaultPasswordResetProvider = _passwordResetProviders.OfType().First(); } /// @@ -557,17 +567,6 @@ namespace Jellyfin.Server.Implementations.Users }; } - /// - public void AddParts(IEnumerable authenticationProviders, IEnumerable passwordResetProviders) - { - _authenticationProviders = authenticationProviders.ToArray(); - _passwordResetProviders = passwordResetProviders.ToArray(); - - _invalidAuthProvider = _authenticationProviders.OfType().First(); - _defaultAuthenticationProvider = _authenticationProviders.OfType().First(); - _defaultPasswordResetProvider = _passwordResetProviders.OfType().First(); - } - /// public void Initialize() { diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index e73fe7120..88b96ddbf 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Jellyfin.Data.Entities; -using MediaBrowser.Controller.Authentication; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Events; @@ -166,8 +165,6 @@ namespace MediaBrowser.Controller.Library /// true if XXXX, false otherwise. Task RedeemPasswordResetPin(string pin); - void AddParts(IEnumerable authenticationProviders, IEnumerable passwordResetProviders); - NameIdPair[] GetAuthenticationProviders(); NameIdPair[] GetPasswordResetProviders(); -- cgit v1.2.3 From 303c175714db50bf865939fd12c66dcbda229431 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sat, 20 Jun 2020 17:58:09 -0400 Subject: Fix circular dependency --- Emby.Server.Implementations/ApplicationHost.cs | 3 --- .../Users/DefaultPasswordResetProvider.cs | 16 +++++++++------- Jellyfin.Server.Implementations/Users/UserManager.cs | 17 +++++++++-------- 3 files changed, 18 insertions(+), 18 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 908fe4b30..1ff14bdb5 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -547,9 +547,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(ServerConfigurationManager); - serviceCollection.AddSingleton>>(() => GetExports()); - serviceCollection.AddSingleton>>(() => GetExports()); - serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs index cf5a01f08..007c29643 100644 --- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs +++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs @@ -6,6 +6,7 @@ using System.IO; using System.Security.Cryptography; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using MediaBrowser.Common; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Configuration; @@ -23,7 +24,7 @@ namespace Jellyfin.Server.Implementations.Users private const string BaseResetFileName = "passwordreset"; private readonly IJsonSerializer _jsonSerializer; - private readonly IUserManager _userManager; + private readonly IApplicationHost _appHost; private readonly string _passwordResetFileBase; private readonly string _passwordResetFileBaseDir; @@ -33,16 +34,17 @@ namespace Jellyfin.Server.Implementations.Users /// /// The configuration manager. /// The JSON serializer. - /// The user manager. + /// The application host. public DefaultPasswordResetProvider( IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, - IUserManager userManager) + IApplicationHost appHost) { _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath; _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName); _jsonSerializer = jsonSerializer; - _userManager = userManager; + _appHost = appHost; + // TODO: Remove the circular dependency on UserManager } /// @@ -54,6 +56,7 @@ namespace Jellyfin.Server.Implementations.Users /// public async Task RedeemPasswordResetPin(string pin) { + var userManager = _appHost.Resolve(); var usersReset = new List(); foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*")) { @@ -72,10 +75,10 @@ namespace Jellyfin.Server.Implementations.Users pin.Replace("-", string.Empty, StringComparison.Ordinal), StringComparison.InvariantCultureIgnoreCase)) { - var resetUser = _userManager.GetUserByName(spr.UserName) + var resetUser = userManager.GetUserByName(spr.UserName) ?? throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found"); - await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); + await userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); usersReset.Add(resetUser.Username); File.Delete(resetFile); } @@ -121,7 +124,6 @@ namespace Jellyfin.Server.Implementations.Users } user.EasyPassword = pin; - await _userManager.UpdateUserAsync(user).ConfigureAwait(false); return new ForgotPasswordResult { diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index d4b91f3c5..988b0c430 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -54,17 +54,13 @@ namespace Jellyfin.Server.Implementations.Users /// The application host. /// The image processor. /// The logger. - /// A function that returns available password reset providers. - /// A function that returns available authentication providers. public UserManager( JellyfinDbProvider dbProvider, ICryptoProvider cryptoProvider, INetworkManager networkManager, IApplicationHost appHost, IImageProcessor imageProcessor, - ILogger logger, - Func> passwordResetProviders, - Func> authenticationProviders) + ILogger logger) { _dbProvider = dbProvider; _cryptoProvider = cryptoProvider; @@ -73,8 +69,8 @@ namespace Jellyfin.Server.Implementations.Users _imageProcessor = imageProcessor; _logger = logger; - _passwordResetProviders = passwordResetProviders.Invoke(); - _authenticationProviders = authenticationProviders.Invoke(); + _passwordResetProviders = appHost.GetExports(); + _authenticationProviders = appHost.GetExports(); _invalidAuthProvider = _authenticationProviders.OfType().First(); _defaultAuthenticationProvider = _authenticationProviders.OfType().First(); @@ -537,7 +533,12 @@ namespace Jellyfin.Server.Implementations.Users if (user != null && isInNetwork) { var passwordResetProvider = GetPasswordResetProvider(user); - return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false); + var result = await passwordResetProvider + .StartForgotPasswordProcess(user, isInNetwork) + .ConfigureAwait(false); + + await UpdateUserAsync(user).ConfigureAwait(false); + return result; } return new ForgotPasswordResult -- cgit v1.2.3 From ab396225eaf486932fdb2f23eefa1cbfecbb27f4 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 30 Jun 2020 21:44:41 -0400 Subject: Migrate Display Preferences to EF Core --- Emby.Dlna/ContentDirectory/ControlHandler.cs | 1 + Emby.Server.Implementations/ApplicationHost.cs | 3 - .../Channels/ChannelManager.cs | 1 + .../Data/SqliteDisplayPreferencesRepository.cs | 225 ------------ .../Data/SqliteItemRepository.cs | 1 + .../Emby.Server.Implementations.csproj | 2 +- .../Images/CollectionFolderImageProvider.cs | 2 +- .../Images/FolderImageProvider.cs | 2 +- .../Images/GenreImageProvider.cs | 1 + .../Library/LibraryManager.cs | 1 - .../Library/MusicManager.cs | 2 +- .../Library/SearchEngine.cs | 2 +- .../LiveTv/EmbyTV/EmbyTV.cs | 1 + Jellyfin.Data/Entities/DisplayPreferences.cs | 72 ++++ Jellyfin.Data/Entities/HomeSection.cs | 21 ++ Jellyfin.Data/Entities/User.cs | 5 + Jellyfin.Data/Enums/HomeSectionType.cs | 53 +++ Jellyfin.Data/Enums/IndexingKind.cs | 20 + Jellyfin.Data/Enums/ScrollDirection.cs | 18 + Jellyfin.Data/Enums/SortOrder.cs | 18 + Jellyfin.Data/Enums/ViewType.cs | 38 ++ .../DisplayPreferencesManager.cs | 49 +++ Jellyfin.Server.Implementations/JellyfinDb.cs | 2 + ...0200630170339_AddDisplayPreferences.Designer.cs | 403 +++++++++++++++++++++ .../20200630170339_AddDisplayPreferences.cs | 92 +++++ .../Migrations/JellyfinDbModelSnapshot.cs | 93 ++++- Jellyfin.Server/CoreAppHost.cs | 2 + Jellyfin.Server/Migrations/MigrationRunner.cs | 3 +- .../Routines/MigrateDisplayPreferencesDb.cs | 118 ++++++ MediaBrowser.Api/ChannelService.cs | 2 +- MediaBrowser.Api/DisplayPreferencesService.cs | 92 ++++- MediaBrowser.Api/Movies/MoviesService.cs | 1 + MediaBrowser.Api/SuggestionsService.cs | 1 + MediaBrowser.Api/TvShowsService.cs | 1 + MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs | 5 +- .../Entities/UserViewBuilder.cs | 1 + .../IDisplayPreferencesManager.cs | 25 ++ MediaBrowser.Controller/Library/ILibraryManager.cs | 1 + .../Persistence/IDisplayPreferencesRepository.cs | 53 --- MediaBrowser.Controller/Playlists/Playlist.cs | 1 + MediaBrowser.Model/Dlna/SortCriteria.cs | 2 +- MediaBrowser.Model/Entities/DisplayPreferences.cs | 111 ------ .../Entities/DisplayPreferencesDto.cs | 107 ++++++ MediaBrowser.Model/Entities/ScrollDirection.cs | 18 - MediaBrowser.Model/Entities/SortOrder.cs | 18 - MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs | 2 +- MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs | 2 +- 47 files changed, 1232 insertions(+), 462 deletions(-) delete mode 100644 Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs create mode 100644 Jellyfin.Data/Entities/DisplayPreferences.cs create mode 100644 Jellyfin.Data/Entities/HomeSection.cs create mode 100644 Jellyfin.Data/Enums/HomeSectionType.cs create mode 100644 Jellyfin.Data/Enums/IndexingKind.cs create mode 100644 Jellyfin.Data/Enums/ScrollDirection.cs create mode 100644 Jellyfin.Data/Enums/SortOrder.cs create mode 100644 Jellyfin.Data/Enums/ViewType.cs create mode 100644 Jellyfin.Server.Implementations/DisplayPreferencesManager.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20200630170339_AddDisplayPreferences.Designer.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20200630170339_AddDisplayPreferences.cs create mode 100644 Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs create mode 100644 MediaBrowser.Controller/IDisplayPreferencesManager.cs delete mode 100644 MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs delete mode 100644 MediaBrowser.Model/Entities/DisplayPreferences.cs create mode 100644 MediaBrowser.Model/Entities/DisplayPreferencesDto.cs delete mode 100644 MediaBrowser.Model/Entities/ScrollDirection.cs delete mode 100644 MediaBrowser.Model/Entities/SortOrder.cs (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index 291de5245..00821bf78 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -11,6 +11,7 @@ using System.Xml; using Emby.Dlna.Didl; using Emby.Dlna.Service; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index f6077400d..f6f10beb0 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -554,8 +554,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -650,7 +648,6 @@ namespace Emby.Server.Implementations _httpServer = Resolve(); _httpClient = Resolve(); - ((SqliteDisplayPreferencesRepository)Resolve()).Initialize(); ((AuthenticationRepository)Resolve()).Initialize(); SetStaticProperties(); diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index c803d9d82..2a7cddd87 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Channels; diff --git a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs deleted file mode 100644 index 5597155a8..000000000 --- a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs +++ /dev/null @@ -1,225 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Text.Json; -using System.Threading; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Json; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; - -namespace Emby.Server.Implementations.Data -{ - /// - /// Class SQLiteDisplayPreferencesRepository. - /// - public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository - { - private readonly IFileSystem _fileSystem; - - private readonly JsonSerializerOptions _jsonOptions; - - public SqliteDisplayPreferencesRepository(ILogger logger, IApplicationPaths appPaths, IFileSystem fileSystem) - : base(logger) - { - _fileSystem = fileSystem; - - _jsonOptions = JsonDefaults.GetOptions(); - - DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db"); - } - - /// - /// Gets the name of the repository. - /// - /// The name. - public string Name => "SQLite"; - - public void Initialize() - { - try - { - InitializeInternal(); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error loading database file. Will reset and retry."); - - _fileSystem.DeleteFile(DbFilePath); - - InitializeInternal(); - } - } - - /// - /// Opens the connection to the database. - /// - /// Task. - private void InitializeInternal() - { - string[] queries = - { - "create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)", - "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)" - }; - - using (var connection = GetConnection()) - { - connection.RunQueries(queries); - } - } - - /// - /// Save the display preferences associated with an item in the repo. - /// - /// The display preferences. - /// The user id. - /// The client. - /// The cancellation token. - /// item - public void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, CancellationToken cancellationToken) - { - if (displayPreferences == null) - { - throw new ArgumentNullException(nameof(displayPreferences)); - } - - if (string.IsNullOrEmpty(displayPreferences.Id)) - { - throw new ArgumentException("Display preferences has an invalid Id", nameof(displayPreferences)); - } - - cancellationToken.ThrowIfCancellationRequested(); - - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => SaveDisplayPreferences(displayPreferences, userId, client, db), - TransactionMode); - } - } - - private void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, IDatabaseConnection connection) - { - var serialized = JsonSerializer.SerializeToUtf8Bytes(displayPreferences, _jsonOptions); - - using (var statement = connection.PrepareStatement("replace into userdisplaypreferences (id, userid, client, data) values (@id, @userId, @client, @data)")) - { - statement.TryBind("@id", new Guid(displayPreferences.Id).ToByteArray()); - statement.TryBind("@userId", userId.ToByteArray()); - statement.TryBind("@client", client); - statement.TryBind("@data", serialized); - - statement.MoveNext(); - } - } - - /// - /// Save all display preferences associated with a user in the repo. - /// - /// The display preferences. - /// The user id. - /// The cancellation token. - /// item - public void SaveAllDisplayPreferences(IEnumerable displayPreferences, Guid userId, CancellationToken cancellationToken) - { - if (displayPreferences == null) - { - throw new ArgumentNullException(nameof(displayPreferences)); - } - - cancellationToken.ThrowIfCancellationRequested(); - - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => - { - foreach (var displayPreference in displayPreferences) - { - SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db); - } - }, - TransactionMode); - } - } - - /// - /// Gets the display preferences. - /// - /// The display preferences id. - /// The user id. - /// The client. - /// Task{DisplayPreferences}. - /// item - public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, Guid userId, string client) - { - if (string.IsNullOrEmpty(displayPreferencesId)) - { - throw new ArgumentNullException(nameof(displayPreferencesId)); - } - - var guidId = displayPreferencesId.GetMD5(); - - using (var connection = GetConnection(true)) - { - using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where id = @id and userId=@userId and client=@client")) - { - statement.TryBind("@id", guidId.ToByteArray()); - statement.TryBind("@userId", userId.ToByteArray()); - statement.TryBind("@client", client); - - foreach (var row in statement.ExecuteQuery()) - { - return Get(row); - } - } - } - - return new DisplayPreferences - { - Id = guidId.ToString("N", CultureInfo.InvariantCulture) - }; - } - - /// - /// Gets all display preferences for the given user. - /// - /// The user id. - /// Task{DisplayPreferences}. - /// item - public IEnumerable GetAllDisplayPreferences(Guid userId) - { - var list = new List(); - - using (var connection = GetConnection(true)) - using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId")) - { - statement.TryBind("@userId", userId.ToByteArray()); - - foreach (var row in statement.ExecuteQuery()) - { - list.Add(Get(row)); - } - } - - return list; - } - - private DisplayPreferences Get(IReadOnlyList row) - => JsonSerializer.Deserialize(row[0].ToBlob(), _jsonOptions); - - public void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken) - => SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken); - - public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client) - => GetDisplayPreferences(displayPreferencesId, new Guid(userId), client); - } -} diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index a6390b1ef..04e5e570f 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -9,6 +9,7 @@ using System.Text; using System.Text.Json; using System.Threading; using Emby.Server.Implementations.Playlists; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Json; using MediaBrowser.Controller; diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index f7ad59c10..548dc7085 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -54,7 +54,7 @@ netstandard2.1 false true - true + true diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs index da88b8d8a..161b4c452 100644 --- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.IO; -using Emby.Server.Implementations.Images; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; diff --git a/Emby.Server.Implementations/Images/FolderImageProvider.cs b/Emby.Server.Implementations/Images/FolderImageProvider.cs index e9523386e..0224ab32a 100644 --- a/Emby.Server.Implementations/Images/FolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/FolderImageProvider.cs @@ -1,7 +1,7 @@ #pragma warning disable CS1591 using System.Collections.Generic; -using Emby.Server.Implementations.Images; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs index d2aeccdb2..1cd4cd66b 100644 --- a/Emby.Server.Implementations/Images/GenreImageProvider.cs +++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System.Collections.Generic; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 77d44e131..4690f2094 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -50,7 +50,6 @@ using Microsoft.Extensions.Logging; using Episode = MediaBrowser.Controller.Entities.TV.Episode; using Genre = MediaBrowser.Controller.Entities.Genre; using Person = MediaBrowser.Controller.Entities.Person; -using SortOrder = MediaBrowser.Model.Entities.SortOrder; using VideoResolver = Emby.Naming.Video.VideoResolver; namespace Emby.Server.Implementations.Library diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index 0bdc59914..877fdec86 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -4,12 +4,12 @@ using System; using System.Collections.Generic; using System.Linq; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index 3df9cc06f..e3e554824 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -4,12 +4,12 @@ using System; using System.Collections.Generic; using System.Linq; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Search; using Microsoft.Extensions.Logging; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 7b0fcbc9e..80e09f0a3 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using System.Xml; using Emby.Server.Implementations.Library; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; diff --git a/Jellyfin.Data/Entities/DisplayPreferences.cs b/Jellyfin.Data/Entities/DisplayPreferences.cs new file mode 100644 index 000000000..668030149 --- /dev/null +++ b/Jellyfin.Data/Entities/DisplayPreferences.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Enums; + +namespace Jellyfin.Data.Entities +{ + public class DisplayPreferences + { + public DisplayPreferences(string client, Guid userId) + { + RememberIndexing = false; + ShowBackdrop = true; + Client = client; + UserId = userId; + + HomeSections = new HashSet(); + } + + protected DisplayPreferences() + { + } + + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + [Required] + public Guid UserId { get; set; } + + /// + /// Gets or sets the id of the associated item. + /// + /// + /// This is currently unused. In the future, this will allow us to have users set + /// display preferences per item. + /// + public Guid? ItemId { get; set; } + + [Required] + [MaxLength(64)] + [StringLength(64)] + public string Client { get; set; } + + [Required] + public bool RememberIndexing { get; set; } + + [Required] + public bool RememberSorting { get; set; } + + [Required] + public SortOrder SortOrder { get; set; } + + [Required] + public bool ShowSidebar { get; set; } + + [Required] + public bool ShowBackdrop { get; set; } + + public string SortBy { get; set; } + + public ViewType? ViewType { get; set; } + + [Required] + public ScrollDirection ScrollDirection { get; set; } + + public IndexingKind? IndexBy { get; set; } + + public virtual ICollection HomeSections { get; protected set; } + } +} diff --git a/Jellyfin.Data/Entities/HomeSection.cs b/Jellyfin.Data/Entities/HomeSection.cs new file mode 100644 index 000000000..f39956a54 --- /dev/null +++ b/Jellyfin.Data/Entities/HomeSection.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Enums; + +namespace Jellyfin.Data.Entities +{ + public class HomeSection + { + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + public int DisplayPreferencesId { get; set; } + + public int Order { get; set; } + + public HomeSectionType Type { get; set; } + } +} diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs index b89b0a8f4..d93144e3a 100644 --- a/Jellyfin.Data/Entities/User.cs +++ b/Jellyfin.Data/Entities/User.cs @@ -349,6 +349,11 @@ namespace Jellyfin.Data.Entities /// public virtual ICollection AccessSchedules { get; protected set; } + /// + /// Gets or sets the list of item display preferences. + /// + public virtual ICollection DisplayPreferences { get; protected set; } + /* /// /// Gets or sets the list of groups this user is a member of. diff --git a/Jellyfin.Data/Enums/HomeSectionType.cs b/Jellyfin.Data/Enums/HomeSectionType.cs new file mode 100644 index 000000000..be764c592 --- /dev/null +++ b/Jellyfin.Data/Enums/HomeSectionType.cs @@ -0,0 +1,53 @@ +namespace Jellyfin.Data.Enums +{ + /// + /// An enum representing the different options for the home screen sections. + /// + public enum HomeSectionType + { + /// + /// My Media. + /// + SmallLibraryTiles = 0, + + /// + /// My Media Small. + /// + LibraryButtons = 1, + + /// + /// Active Recordings. + /// + ActiveRecordings = 2, + + /// + /// Continue Watching. + /// + Resume = 3, + + /// + /// Continue Listening. + /// + ResumeAudio = 4, + + /// + /// Latest Media. + /// + LatestMedia = 5, + + /// + /// Next Up. + /// + NextUp = 6, + + /// + /// Live TV. + /// + LiveTv = 7, + + /// + /// None. + /// + None = 8 + } +} diff --git a/Jellyfin.Data/Enums/IndexingKind.cs b/Jellyfin.Data/Enums/IndexingKind.cs new file mode 100644 index 000000000..c4d8e70ca --- /dev/null +++ b/Jellyfin.Data/Enums/IndexingKind.cs @@ -0,0 +1,20 @@ +namespace Jellyfin.Data.Enums +{ + public enum IndexingKind + { + /// + /// Index by the premiere date. + /// + PremiereDate, + + /// + /// Index by the production year. + /// + ProductionYear, + + /// + /// Index by the community rating. + /// + CommunityRating + } +} diff --git a/Jellyfin.Data/Enums/ScrollDirection.cs b/Jellyfin.Data/Enums/ScrollDirection.cs new file mode 100644 index 000000000..382f585ba --- /dev/null +++ b/Jellyfin.Data/Enums/ScrollDirection.cs @@ -0,0 +1,18 @@ +namespace Jellyfin.Data.Enums +{ + /// + /// An enum representing the axis that should be scrolled. + /// + public enum ScrollDirection + { + /// + /// Horizontal scrolling direction. + /// + Horizontal, + + /// + /// Vertical scrolling direction. + /// + Vertical + } +} diff --git a/Jellyfin.Data/Enums/SortOrder.cs b/Jellyfin.Data/Enums/SortOrder.cs new file mode 100644 index 000000000..309fa7877 --- /dev/null +++ b/Jellyfin.Data/Enums/SortOrder.cs @@ -0,0 +1,18 @@ +namespace Jellyfin.Data.Enums +{ + /// + /// An enum representing the sorting order. + /// + public enum SortOrder + { + /// + /// Sort in increasing order. + /// + Ascending, + + /// + /// Sort in decreasing order. + /// + Descending + } +} diff --git a/Jellyfin.Data/Enums/ViewType.cs b/Jellyfin.Data/Enums/ViewType.cs new file mode 100644 index 000000000..595429ab1 --- /dev/null +++ b/Jellyfin.Data/Enums/ViewType.cs @@ -0,0 +1,38 @@ +namespace Jellyfin.Data.Enums +{ + /// + /// An enum representing the type of view for a library or collection. + /// + public enum ViewType + { + /// + /// Shows banners. + /// + Banner = 0, + + /// + /// Shows a list of content. + /// + List = 1, + + /// + /// Shows poster artwork. + /// + Poster = 2, + + /// + /// Shows poster artwork with a card containing the name and year. + /// + PosterCard = 3, + + /// + /// Shows a thumbnail. + /// + Thumb = 4, + + /// + /// Shows a thumbnail with a card containing the name and year. + /// + ThumbCard = 5 + } +} diff --git a/Jellyfin.Server.Implementations/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/DisplayPreferencesManager.cs new file mode 100644 index 000000000..132e74c6a --- /dev/null +++ b/Jellyfin.Server.Implementations/DisplayPreferencesManager.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller; + +namespace Jellyfin.Server.Implementations +{ + /// + /// Manages the storage and retrieval of display preferences through Entity Framework. + /// + public class DisplayPreferencesManager : IDisplayPreferencesManager + { + private readonly JellyfinDbProvider _dbProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The Jellyfin db provider. + public DisplayPreferencesManager(JellyfinDbProvider dbProvider) + { + _dbProvider = dbProvider; + } + + /// + public DisplayPreferences GetDisplayPreferences(Guid userId, string client) + { + var dbContext = _dbProvider.CreateContext(); + var user = dbContext.Users.Find(userId); +#pragma warning disable CA1307 + var prefs = user.DisplayPreferences.FirstOrDefault(pref => string.Equals(pref.Client, client)); + + if (prefs == null) + { + prefs = new DisplayPreferences(client, userId); + user.DisplayPreferences.Add(prefs); + } + + return prefs; + } + + /// + public void SaveChanges(DisplayPreferences preferences) + { + var dbContext = _dbProvider.CreateContext(); + dbContext.Update(preferences); + dbContext.SaveChanges(); + } + } +} diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index 53120a763..774970e94 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -27,6 +27,8 @@ namespace Jellyfin.Server.Implementations public virtual DbSet ActivityLogs { get; set; } + public virtual DbSet DisplayPreferences { get; set; } + public virtual DbSet ImageInfos { get; set; } public virtual DbSet Permissions { get; set; } diff --git a/Jellyfin.Server.Implementations/Migrations/20200630170339_AddDisplayPreferences.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200630170339_AddDisplayPreferences.Designer.cs new file mode 100644 index 000000000..75f9bb7a3 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20200630170339_AddDisplayPreferences.Designer.cs @@ -0,0 +1,403 @@ +#pragma warning disable CS1591 + +// +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDb))] + [Migration("20200630170339_AddDisplayPreferences")] + partial class AddDisplayPreferences + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("jellyfin") + .HasAnnotation("ProductVersion", "3.1.5"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("Overview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Permission_Permissions_Guid"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.HasKey("Id"); + + b.HasIndex("Preference_Preferences_Guid"); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EasyPassword") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("Permission_Permissions_Guid"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("Preference_Preferences_Guid"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20200630170339_AddDisplayPreferences.cs b/Jellyfin.Server.Implementations/Migrations/20200630170339_AddDisplayPreferences.cs new file mode 100644 index 000000000..e9a493d9d --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20200630170339_AddDisplayPreferences.cs @@ -0,0 +1,92 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1601 + +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Jellyfin.Server.Implementations.Migrations +{ + public partial class AddDisplayPreferences : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "DisplayPreferences", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(nullable: false), + ItemId = table.Column(nullable: true), + Client = table.Column(maxLength: 64, nullable: false), + RememberIndexing = table.Column(nullable: false), + RememberSorting = table.Column(nullable: false), + SortOrder = table.Column(nullable: false), + ShowSidebar = table.Column(nullable: false), + ShowBackdrop = table.Column(nullable: false), + SortBy = table.Column(nullable: true), + ViewType = table.Column(nullable: true), + ScrollDirection = table.Column(nullable: false), + IndexBy = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_DisplayPreferences", x => x.Id); + table.ForeignKey( + name: "FK_DisplayPreferences_Users_UserId", + column: x => x.UserId, + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "HomeSection", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DisplayPreferencesId = table.Column(nullable: false), + Order = table.Column(nullable: false), + Type = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_HomeSection", x => x.Id); + table.ForeignKey( + name: "FK_HomeSection_DisplayPreferences_DisplayPreferencesId", + column: x => x.DisplayPreferencesId, + principalSchema: "jellyfin", + principalTable: "DisplayPreferences", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_DisplayPreferences_UserId", + schema: "jellyfin", + table: "DisplayPreferences", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_HomeSection_DisplayPreferencesId", + schema: "jellyfin", + table: "HomeSection", + column: "DisplayPreferencesId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "HomeSection", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "DisplayPreferences", + schema: "jellyfin"); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index 51fad8224..69b544e5b 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("jellyfin") - .HasAnnotation("ProductVersion", "3.1.4"); + .HasAnnotation("ProductVersion", "3.1.5"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -88,6 +88,79 @@ namespace Jellyfin.Server.Implementations.Migrations b.ToTable("ActivityLogs"); }); + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(64); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => { b.Property("Id") @@ -282,6 +355,24 @@ namespace Jellyfin.Server.Implementations.Migrations .IsRequired(); }); + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => { b.HasOne("Jellyfin.Data.Entities.User", null) diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 207eaa98d..c5a7368aa 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -9,6 +9,7 @@ using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Activity; using Jellyfin.Server.Implementations.Users; using MediaBrowser.Common.Net; +using MediaBrowser.Controller; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Activity; @@ -73,6 +74,7 @@ namespace Jellyfin.Server serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); base.RegisterServices(serviceCollection); } diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index d633c554d..7f208952c 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -21,7 +21,8 @@ namespace Jellyfin.Server.Migrations typeof(Routines.MigrateActivityLogDb), typeof(Routines.RemoveDuplicateExtras), typeof(Routines.AddDefaultPluginRepository), - typeof(Routines.MigrateUserDb) + typeof(Routines.MigrateUserDb), + typeof(Routines.MigrateDisplayPreferencesDb) }; /// diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs new file mode 100644 index 000000000..1ed23fe8e --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs @@ -0,0 +1,118 @@ +using System; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using Jellyfin.Server.Implementations; +using MediaBrowser.Controller; +using MediaBrowser.Model.Entities; +using Microsoft.Extensions.Logging; +using SQLitePCL.pretty; + +namespace Jellyfin.Server.Migrations.Routines +{ + /// + /// The migration routine for migrating the display preferences database to EF Core. + /// + public class MigrateDisplayPreferencesDb : IMigrationRoutine + { + private const string DbFilename = "displaypreferences.db"; + + private readonly ILogger _logger; + private readonly IServerApplicationPaths _paths; + private readonly JellyfinDbProvider _provider; + private readonly JsonSerializerOptions _jsonOptions; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The server application paths. + /// The database provider. + public MigrateDisplayPreferencesDb(ILogger logger, IServerApplicationPaths paths, JellyfinDbProvider provider) + { + _logger = logger; + _paths = paths; + _provider = provider; + _jsonOptions = new JsonSerializerOptions(); + _jsonOptions.Converters.Add(new JsonStringEnumConverter()); + } + + /// + public Guid Id => Guid.Parse("06387815-C3CC-421F-A888-FB5F9992BEA8"); + + /// + public string Name => "MigrateDisplayPreferencesDatabase"; + + /// + public void Perform() + { + HomeSectionType[] defaults = + { + HomeSectionType.SmallLibraryTiles, + HomeSectionType.Resume, + HomeSectionType.ResumeAudio, + HomeSectionType.LiveTv, + HomeSectionType.NextUp, + HomeSectionType.LatestMedia, + HomeSectionType.None, + }; + + var dbFilePath = Path.Combine(_paths.DataPath, DbFilename); + using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null)) + { + var dbContext = _provider.CreateContext(); + + var results = connection.Query("SELECT * FROM userdisplaypreferences"); + foreach (var result in results) + { + var dto = JsonSerializer.Deserialize(result[3].ToString(), _jsonOptions); + + var displayPreferences = new DisplayPreferences(result[2].ToString(), new Guid(result[1].ToBlob())) + { + ViewType = Enum.TryParse(dto.ViewType, true, out var viewType) ? viewType : (ViewType?)null, + IndexBy = Enum.TryParse(dto.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null, + ShowBackdrop = dto.ShowBackdrop, + ShowSidebar = dto.ShowSidebar, + SortBy = dto.SortBy, + SortOrder = dto.SortOrder, + RememberIndexing = dto.RememberIndexing, + RememberSorting = dto.RememberSorting, + ScrollDirection = dto.ScrollDirection + }; + + for (int i = 0; i < 7; i++) + { + dto.CustomPrefs.TryGetValue("homesection" + i, out var homeSection); + + displayPreferences.HomeSections.Add(new HomeSection + { + Order = i, + Type = Enum.TryParse(homeSection, true, out var type) ? type : defaults[i] + }); + } + + dbContext.Add(displayPreferences); + } + + dbContext.SaveChanges(); + } + + try + { + File.Move(dbFilePath, dbFilePath + ".old"); + + var journalPath = dbFilePath + "-journal"; + if (File.Exists(journalPath)) + { + File.Move(journalPath, dbFilePath + ".old-journal"); + } + } + catch (IOException e) + { + _logger.LogError(e, "Error renaming legacy display preferences database to 'displaypreferences.db.old'"); + } + } + } +} diff --git a/MediaBrowser.Api/ChannelService.cs b/MediaBrowser.Api/ChannelService.cs index 8c336b1c9..8d3a9ee5a 100644 --- a/MediaBrowser.Api/ChannelService.cs +++ b/MediaBrowser.Api/ChannelService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Api.UserLibrary; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; @@ -11,7 +12,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Channels; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; diff --git a/MediaBrowser.Api/DisplayPreferencesService.cs b/MediaBrowser.Api/DisplayPreferencesService.cs index c3ed40ad3..e5dd03807 100644 --- a/MediaBrowser.Api/DisplayPreferencesService.cs +++ b/MediaBrowser.Api/DisplayPreferencesService.cs @@ -1,9 +1,10 @@ -using System.Threading; +using System; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; @@ -13,7 +14,7 @@ namespace MediaBrowser.Api /// Class UpdateDisplayPreferences. /// [Route("/DisplayPreferences/{DisplayPreferencesId}", "POST", Summary = "Updates a user's display preferences for an item")] - public class UpdateDisplayPreferences : DisplayPreferences, IReturnVoid + public class UpdateDisplayPreferences : DisplayPreferencesDto, IReturnVoid { /// /// Gets or sets the id. @@ -27,7 +28,7 @@ namespace MediaBrowser.Api } [Route("/DisplayPreferences/{Id}", "GET", Summary = "Gets a user's display preferences for an item")] - public class GetDisplayPreferences : IReturn + public class GetDisplayPreferences : IReturn { /// /// Gets or sets the id. @@ -50,28 +51,21 @@ namespace MediaBrowser.Api public class DisplayPreferencesService : BaseApiService { /// - /// The _display preferences manager. + /// The user manager. /// - private readonly IDisplayPreferencesRepository _displayPreferencesManager; - /// - /// The _json serializer. - /// - private readonly IJsonSerializer _jsonSerializer; + private readonly IDisplayPreferencesManager _displayPreferencesManager; /// /// Initializes a new instance of the class. /// - /// The json serializer. /// The display preferences manager. public DisplayPreferencesService( ILogger logger, IServerConfigurationManager serverConfigurationManager, IHttpResultFactory httpResultFactory, - IJsonSerializer jsonSerializer, - IDisplayPreferencesRepository displayPreferencesManager) + IDisplayPreferencesManager displayPreferencesManager) : base(logger, serverConfigurationManager, httpResultFactory) { - _jsonSerializer = jsonSerializer; _displayPreferencesManager = displayPreferencesManager; } @@ -81,9 +75,34 @@ namespace MediaBrowser.Api /// The request. public object Get(GetDisplayPreferences request) { - var result = _displayPreferencesManager.GetDisplayPreferences(request.Id, request.UserId, request.Client); + var result = _displayPreferencesManager.GetDisplayPreferences(Guid.Parse(request.UserId), request.Client); + + if (result == null) + { + return null; + } + + var dto = new DisplayPreferencesDto + { + Client = result.Client, + Id = result.UserId.ToString(), + ViewType = result.ViewType?.ToString(), + SortBy = result.SortBy, + SortOrder = result.SortOrder, + IndexBy = result.IndexBy?.ToString(), + RememberIndexing = result.RememberIndexing, + RememberSorting = result.RememberSorting, + ScrollDirection = result.ScrollDirection, + ShowBackdrop = result.ShowBackdrop, + ShowSidebar = result.ShowSidebar + }; - return ToOptimizedResult(result); + foreach (var homeSection in result.HomeSections) + { + dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant(); + } + + return ToOptimizedResult(dto); } /// @@ -92,10 +111,43 @@ namespace MediaBrowser.Api /// The request. public void Post(UpdateDisplayPreferences request) { - // Serialize to json and then back so that the core doesn't see the request dto type - var displayPreferences = _jsonSerializer.DeserializeFromString(_jsonSerializer.SerializeToString(request)); + HomeSectionType[] defaults = + { + HomeSectionType.SmallLibraryTiles, + HomeSectionType.Resume, + HomeSectionType.ResumeAudio, + HomeSectionType.LiveTv, + HomeSectionType.NextUp, + HomeSectionType.LatestMedia, + HomeSectionType.None, + }; + + var prefs = _displayPreferencesManager.GetDisplayPreferences(Guid.Parse(request.UserId), request.Client); + + prefs.ViewType = Enum.TryParse(request.ViewType, true, out var viewType) ? viewType : (ViewType?)null; + prefs.IndexBy = Enum.TryParse(request.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null; + prefs.ShowBackdrop = request.ShowBackdrop; + prefs.ShowSidebar = request.ShowSidebar; + prefs.SortBy = request.SortBy; + prefs.SortOrder = request.SortOrder; + prefs.RememberIndexing = request.RememberIndexing; + prefs.RememberSorting = request.RememberSorting; + prefs.ScrollDirection = request.ScrollDirection; + prefs.HomeSections.Clear(); + + for (int i = 0; i < 7; i++) + { + if (request.CustomPrefs.TryGetValue("homesection" + i, out var homeSection)) + { + prefs.HomeSections.Add(new HomeSection + { + Order = i, + Type = Enum.TryParse(homeSection, true, out var type) ? type : defaults[i] + }); + } + } - _displayPreferencesManager.SaveDisplayPreferences(displayPreferences, request.UserId, request.Client, CancellationToken.None); + _displayPreferencesManager.SaveChanges(prefs); } } } diff --git a/MediaBrowser.Api/Movies/MoviesService.cs b/MediaBrowser.Api/Movies/MoviesService.cs index 34cccffa3..2ff322d29 100644 --- a/MediaBrowser.Api/Movies/MoviesService.cs +++ b/MediaBrowser.Api/Movies/MoviesService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; diff --git a/MediaBrowser.Api/SuggestionsService.cs b/MediaBrowser.Api/SuggestionsService.cs index 17afa8e79..b42e822e8 100644 --- a/MediaBrowser.Api/SuggestionsService.cs +++ b/MediaBrowser.Api/SuggestionsService.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Api/TvShowsService.cs b/MediaBrowser.Api/TvShowsService.cs index 165abd613..799cea648 100644 --- a/MediaBrowser.Api/TvShowsService.cs +++ b/MediaBrowser.Api/TvShowsService.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs index 344861a49..fc19575b3 100644 --- a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs +++ b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Services; @@ -466,8 +467,8 @@ namespace MediaBrowser.Api.UserLibrary var sortOrderValue = sortOrders.Length > sortOrderIndex ? sortOrders[sortOrderIndex] : null; var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase) - ? MediaBrowser.Model.Entities.SortOrder.Descending - : MediaBrowser.Model.Entities.SortOrder.Ascending; + ? Jellyfin.Data.Enums.SortOrder.Descending + : Jellyfin.Data.Enums.SortOrder.Ascending; result[i] = new ValueTuple(vals[i], sortOrder); } diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index cb35d6e32..22bb7fd55 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; diff --git a/MediaBrowser.Controller/IDisplayPreferencesManager.cs b/MediaBrowser.Controller/IDisplayPreferencesManager.cs new file mode 100644 index 000000000..e27b0ec7c --- /dev/null +++ b/MediaBrowser.Controller/IDisplayPreferencesManager.cs @@ -0,0 +1,25 @@ +using System; +using Jellyfin.Data.Entities; + +namespace MediaBrowser.Controller +{ + /// + /// Manages the storage and retrieval of display preferences. + /// + public interface IDisplayPreferencesManager + { + /// + /// Gets the display preferences for the user and client. + /// + /// The user's id. + /// The client string. + /// The associated display preferences. + DisplayPreferences GetDisplayPreferences(Guid userId, string client); + + /// + /// Saves changes to the provided display preferences. + /// + /// The display preferences to save. + void SaveChanges(DisplayPreferences preferences); + } +} diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 9d6646857..b5eec1846 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; diff --git a/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs b/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs deleted file mode 100644 index c2dcb66d7..000000000 --- a/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Controller.Persistence -{ - /// - /// Interface IDisplayPreferencesRepository. - /// - public interface IDisplayPreferencesRepository : IRepository - { - /// - /// Saves display preferences for an item. - /// - /// The display preferences. - /// The user id. - /// The client. - /// The cancellation token. - void SaveDisplayPreferences( - DisplayPreferences displayPreferences, - string userId, - string client, - CancellationToken cancellationToken); - - /// - /// Saves all display preferences for a user. - /// - /// The display preferences. - /// The user id. - /// The cancellation token. - void SaveAllDisplayPreferences( - IEnumerable displayPreferences, - Guid userId, - CancellationToken cancellationToken); - - /// - /// Gets the display preferences. - /// - /// The display preferences id. - /// The user id. - /// The client. - /// Task{DisplayPreferences}. - DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client); - - /// - /// Gets all display preferences for the given user. - /// - /// The user id. - /// Task{DisplayPreferences}. - IEnumerable GetAllDisplayPreferences(Guid userId); - } -} diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index b1a638883..0fd63770f 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -6,6 +6,7 @@ using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; diff --git a/MediaBrowser.Model/Dlna/SortCriteria.cs b/MediaBrowser.Model/Dlna/SortCriteria.cs index 1f7fa76ad..53e4540cb 100644 --- a/MediaBrowser.Model/Dlna/SortCriteria.cs +++ b/MediaBrowser.Model/Dlna/SortCriteria.cs @@ -1,6 +1,6 @@ #pragma warning disable CS1591 -using MediaBrowser.Model.Entities; +using Jellyfin.Data.Enums; namespace MediaBrowser.Model.Dlna { diff --git a/MediaBrowser.Model/Entities/DisplayPreferences.cs b/MediaBrowser.Model/Entities/DisplayPreferences.cs deleted file mode 100644 index 7e5c5be3b..000000000 --- a/MediaBrowser.Model/Entities/DisplayPreferences.cs +++ /dev/null @@ -1,111 +0,0 @@ -#nullable disable -using System.Collections.Generic; - -namespace MediaBrowser.Model.Entities -{ - /// - /// Defines the display preferences for any item that supports them (usually Folders). - /// - public class DisplayPreferences - { - /// - /// The image scale. - /// - private const double ImageScale = .9; - - /// - /// Initializes a new instance of the class. - /// - public DisplayPreferences() - { - RememberIndexing = false; - PrimaryImageHeight = 250; - PrimaryImageWidth = 250; - ShowBackdrop = true; - CustomPrefs = new Dictionary(); - } - - /// - /// Gets or sets the user id. - /// - /// The user id. - public string Id { get; set; } - - /// - /// Gets or sets the type of the view. - /// - /// The type of the view. - public string ViewType { get; set; } - - /// - /// Gets or sets the sort by. - /// - /// The sort by. - public string SortBy { get; set; } - - /// - /// Gets or sets the index by. - /// - /// The index by. - public string IndexBy { get; set; } - - /// - /// Gets or sets a value indicating whether [remember indexing]. - /// - /// true if [remember indexing]; otherwise, false. - public bool RememberIndexing { get; set; } - - /// - /// Gets or sets the height of the primary image. - /// - /// The height of the primary image. - public int PrimaryImageHeight { get; set; } - - /// - /// Gets or sets the width of the primary image. - /// - /// The width of the primary image. - public int PrimaryImageWidth { get; set; } - - /// - /// Gets or sets the custom prefs. - /// - /// The custom prefs. - public Dictionary CustomPrefs { get; set; } - - /// - /// Gets or sets the scroll direction. - /// - /// The scroll direction. - public ScrollDirection ScrollDirection { get; set; } - - /// - /// Gets or sets a value indicating whether to show backdrops on this item. - /// - /// true if showing backdrops; otherwise, false. - public bool ShowBackdrop { get; set; } - - /// - /// Gets or sets a value indicating whether [remember sorting]. - /// - /// true if [remember sorting]; otherwise, false. - public bool RememberSorting { get; set; } - - /// - /// Gets or sets the sort order. - /// - /// The sort order. - public SortOrder SortOrder { get; set; } - - /// - /// Gets or sets a value indicating whether [show sidebar]. - /// - /// true if [show sidebar]; otherwise, false. - public bool ShowSidebar { get; set; } - - /// - /// Gets or sets the client. - /// - public string Client { get; set; } - } -} diff --git a/MediaBrowser.Model/Entities/DisplayPreferencesDto.cs b/MediaBrowser.Model/Entities/DisplayPreferencesDto.cs new file mode 100644 index 000000000..1f7fe3030 --- /dev/null +++ b/MediaBrowser.Model/Entities/DisplayPreferencesDto.cs @@ -0,0 +1,107 @@ +#nullable disable +using System.Collections.Generic; +using Jellyfin.Data.Enums; + +namespace MediaBrowser.Model.Entities +{ + /// + /// Defines the display preferences for any item that supports them (usually Folders). + /// + public class DisplayPreferencesDto + { + /// + /// Initializes a new instance of the class. + /// + public DisplayPreferencesDto() + { + RememberIndexing = false; + PrimaryImageHeight = 250; + PrimaryImageWidth = 250; + ShowBackdrop = true; + CustomPrefs = new Dictionary(); + } + + /// + /// Gets or sets the user id. + /// + /// The user id. + public string Id { get; set; } + + /// + /// Gets or sets the type of the view. + /// + /// The type of the view. + public string ViewType { get; set; } + + /// + /// Gets or sets the sort by. + /// + /// The sort by. + public string SortBy { get; set; } + + /// + /// Gets or sets the index by. + /// + /// The index by. + public string IndexBy { get; set; } + + /// + /// Gets or sets a value indicating whether [remember indexing]. + /// + /// true if [remember indexing]; otherwise, false. + public bool RememberIndexing { get; set; } + + /// + /// Gets or sets the height of the primary image. + /// + /// The height of the primary image. + public int PrimaryImageHeight { get; set; } + + /// + /// Gets or sets the width of the primary image. + /// + /// The width of the primary image. + public int PrimaryImageWidth { get; set; } + + /// + /// Gets or sets the custom prefs. + /// + /// The custom prefs. + public Dictionary CustomPrefs { get; set; } + + /// + /// Gets or sets the scroll direction. + /// + /// The scroll direction. + public ScrollDirection ScrollDirection { get; set; } + + /// + /// Gets or sets a value indicating whether to show backdrops on this item. + /// + /// true if showing backdrops; otherwise, false. + public bool ShowBackdrop { get; set; } + + /// + /// Gets or sets a value indicating whether [remember sorting]. + /// + /// true if [remember sorting]; otherwise, false. + public bool RememberSorting { get; set; } + + /// + /// Gets or sets the sort order. + /// + /// The sort order. + public SortOrder SortOrder { get; set; } + + /// + /// Gets or sets a value indicating whether [show sidebar]. + /// + /// true if [show sidebar]; otherwise, false. + public bool ShowSidebar { get; set; } + + /// + /// Gets or sets the client. + /// + public string Client { get; set; } + } +} diff --git a/MediaBrowser.Model/Entities/ScrollDirection.cs b/MediaBrowser.Model/Entities/ScrollDirection.cs deleted file mode 100644 index a1de0edcb..000000000 --- a/MediaBrowser.Model/Entities/ScrollDirection.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace MediaBrowser.Model.Entities -{ - /// - /// Enum ScrollDirection. - /// - public enum ScrollDirection - { - /// - /// The horizontal. - /// - Horizontal, - - /// - /// The vertical. - /// - Vertical - } -} diff --git a/MediaBrowser.Model/Entities/SortOrder.cs b/MediaBrowser.Model/Entities/SortOrder.cs deleted file mode 100644 index f3abc06f3..000000000 --- a/MediaBrowser.Model/Entities/SortOrder.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace MediaBrowser.Model.Entities -{ - /// - /// Enum SortOrder. - /// - public enum SortOrder - { - /// - /// The ascending. - /// - Ascending, - - /// - /// The descending. - /// - Descending - } -} diff --git a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs index 2b2377fda..ab74aff28 100644 --- a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs +++ b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs @@ -2,7 +2,7 @@ #pragma warning disable CS1591 using System; -using MediaBrowser.Model.Entities; +using Jellyfin.Data.Enums; namespace MediaBrowser.Model.LiveTv { diff --git a/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs b/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs index b899a464b..dae885775 100644 --- a/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs +++ b/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs @@ -1,6 +1,6 @@ #pragma warning disable CS1591 -using MediaBrowser.Model.Entities; +using Jellyfin.Data.Enums; namespace MediaBrowser.Model.LiveTv { -- cgit v1.2.3 From 3514813eb4eda997a0ea722cc2ed41979419c6dd Mon Sep 17 00:00:00 2001 From: David Date: Sun, 12 Jul 2020 11:14:38 +0200 Subject: Continute work --- Emby.Server.Implementations/ApplicationHost.cs | 3 + Jellyfin.Api/Controllers/AudioController.cs | 350 ++++++++--- Jellyfin.Api/Controllers/PlaystateController.cs | 12 +- Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs | 236 +++++++ Jellyfin.Api/Helpers/StreamingHelpers.cs | 681 +++++++++++++++++++-- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 453 +++++++++++++- Jellyfin.Api/Models/StreamState.cs | 145 ----- Jellyfin.Api/Models/StreamingDtos/StreamState.cs | 207 +++++++ .../Playback/Progressive/AudioService.cs | 4 - .../MediaEncoding/EncodingHelper.cs | 11 + 10 files changed, 1801 insertions(+), 301 deletions(-) create mode 100644 Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs delete mode 100644 Jellyfin.Api/Models/StreamState.cs create mode 100644 Jellyfin.Api/Models/StreamingDtos/StreamState.cs (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 25ee7e9ec..c177537b8 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -46,6 +46,7 @@ using Emby.Server.Implementations.Session; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; using Emby.Server.Implementations.SyncPlay; +using Jellyfin.Api.Helpers; using MediaBrowser.Api; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; @@ -637,6 +638,8 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + + serviceCollection.AddSingleton(); } /// diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 39df1e1b1..4d29d3880 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -3,97 +3,277 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Helpers; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Net; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; +using Microsoft.Extensions.Configuration; namespace Jellyfin.Api.Controllers { - /// /// The audio controller. /// + // TODO: In order to autheneticate this in the future, Dlna playback will require updating public class AudioController : BaseJellyfinApiController { private readonly IDlnaManager _dlnaManager; - private readonly ILogger _logger; + private readonly IAuthorizationContext _authContext; + private readonly IUserManager _userManager; + private readonly ILibraryManager _libraryManager; + private readonly IMediaSourceManager _mediaSourceManager; + private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly IStreamHelper _streamHelper; + private readonly IFileSystem _fileSystem; + private readonly ISubtitleEncoder _subtitleEncoder; + private readonly IConfiguration _configuration; + private readonly IDeviceManager _deviceManager; + private readonly TranscodingJobHelper _transcodingJobHelper; + + private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive; /// /// Initializes a new instance of the class. /// /// Instance of the interface. - /// Instance of the interface. - public AudioController(IDlnaManager dlnaManager, ILogger logger) + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// The singleton. + public AudioController( + IDlnaManager dlnaManager, + IUserManager userManger, + IAuthorizationContext authorizationContext, + ILibraryManager libraryManager, + IMediaSourceManager mediaSourceManager, + IServerConfigurationManager serverConfigurationManager, + IMediaEncoder mediaEncoder, + IStreamHelper streamHelper, + IFileSystem fileSystem, + ISubtitleEncoder subtitleEncoder, + IConfiguration configuration, + IDeviceManager deviceManager, + TranscodingJobHelper transcodingJobHelper) { _dlnaManager = dlnaManager; - _logger = logger; + _authContext = authorizationContext; + _userManager = userManger; + _libraryManager = libraryManager; + _mediaSourceManager = mediaSourceManager; + _serverConfigurationManager = serverConfigurationManager; + _mediaEncoder = mediaEncoder; + _streamHelper = streamHelper; + _fileSystem = fileSystem; + _subtitleEncoder = subtitleEncoder; + _configuration = configuration; + _deviceManager = deviceManager; + _transcodingJobHelper = transcodingJobHelper; } - [HttpGet("{id}/stream.{container}")] - [HttpGet("{id}/stream")] - [HttpHead("{id}/stream.{container}")] - [HttpGet("{id}/stream")] + /// + /// Gets an audio stream. + /// + /// The item id. + /// The audio container. + /// Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false. + /// The streaming parameters. + /// The tag. + /// Optional. The dlna device profile id to utilize. + /// The play session id. + /// The segment container. + /// The segment lenght. + /// The minimum number of segments. + /// The media version id, if playing an alternate version. + /// The device id of the client requesting. Used to stop encoding processes when needed. + /// Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma. + /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. + /// Whether or not to allow copying of the video stream url. + /// Whether or not to allow copying of the audio stream url. + /// Optional. Whether to break on non key frames. + /// Optional. Specify a specific audio sample rate, e.g. 44100. + /// Optional. The maximum audio bit depth. + /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. + /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. + /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. + /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. + /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. + /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. + /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. + /// Optional. The fixed horizontal resolution of the encoded video. + /// Optional. The fixed vertical resolution of the encoded video. + /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. + /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. + /// Optional. Specify the subtitle delivery method. + /// Optional. + /// Optional. The maximum video bit depth. + /// Optional. Whether to require avc. + /// Optional. Whether to deinterlace the video. + /// Optional. Whether to require a non anamporphic stream. + /// Optional. The maximum number of audio channels to transcode. + /// Optional. The limit of how many cpu cores to use. + /// The live stream id. + /// Optional. Whether to enable the MpegtsM2Ts mode. + /// Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv. + /// Optional. Specify a subtitle codec to encode to. + /// Optional. The transcoding reason. + /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. + /// Optional. The index of the video stream to use. If omitted the first video stream will be used. + /// Optional. The . + /// Optional. The streaming options. + /// A containing the audio file. + [HttpGet("{itemId}/stream.{container}")] + [HttpGet("{itemId}/stream")] + [HttpHead("{itemId}/stream.{container}")] + [HttpGet("{itemId}/stream")] + [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetAudioStream( - [FromRoute] string id, - [FromRoute] string container, - [FromQuery] bool Static, - [FromQuery] string tag) + [FromRoute] Guid itemId, + [FromRoute] string? container, + [FromQuery] bool? @static, + [FromQuery] string? @params, + [FromQuery] string? tag, + [FromQuery] string? deviceProfileId, + [FromQuery] string? playSessionId, + [FromQuery] string? segmentContainer, + [FromQuery] int? segmentLength, + [FromQuery] int? minSegments, + [FromQuery] string? mediaSourceId, + [FromQuery] string? deviceId, + [FromQuery] string? audioCodec, + [FromQuery] bool? enableAutoStreamCopy, + [FromQuery] bool? allowVideoStreamCopy, + [FromQuery] bool? allowAudioStreamCopy, + [FromQuery] bool? breakOnNonKeyFrames, + [FromQuery] int? audioSampleRate, + [FromQuery] int? maxAudioBitDepth, + [FromQuery] int? audioBitRate, + [FromQuery] int? audioChannels, + [FromQuery] int? maxAudioChannels, + [FromQuery] string? profile, + [FromQuery] string? level, + [FromQuery] float? framerate, + [FromQuery] float? maxFramerate, + [FromQuery] bool? copyTimestamps, + [FromQuery] long? startTimeTicks, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? videoBitRate, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] int? maxRefFrames, + [FromQuery] int? maxVideoBitDepth, + [FromQuery] bool? requireAvc, + [FromQuery] bool? deInterlace, + [FromQuery] bool? requireNonAnamorphic, + [FromQuery] int? transcodingMaxAudioChannels, + [FromQuery] int? cpuCoreLimit, + [FromQuery] string? liveStreamId, + [FromQuery] bool? enableMpegtsM2TsMode, + [FromQuery] string? videoCodec, + [FromQuery] string? subtitleCodec, + [FromQuery] string? transcodingReasons, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? videoStreamIndex, + [FromQuery] EncodingContext context, + [FromQuery] Dictionary streamOptions) { bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head; var cancellationTokenSource = new CancellationTokenSource(); - var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false); + var state = await StreamingHelpers.GetStreamingState( + itemId, + startTimeTicks, + audioCodec, + subtitleCodec, + videoCodec, + @params, + @static, + container, + liveStreamId, + playSessionId, + mediaSourceId, + deviceId, + deviceProfileId, + audioBitRate, + Request, + _authContext, + _mediaSourceManager, + _userManager, + _libraryManager, + _serverConfigurationManager, + _mediaEncoder, + _fileSystem, + _subtitleEncoder, + _configuration, + _dlnaManager, + _deviceManager, + _transcodingJobHelper, + _transcodingJobType, + false, + cancellationTokenSource.Token) + .ConfigureAwait(false); - if (Static && state.DirectStreamProvider != null) + if (@static.HasValue && @static.Value && state.DirectStreamProvider != null) { - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, Request, _dlnaManager); + StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager); using (state) { - var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - - // TODO: Don't hardcode this - outputHeaders[HeaderNames.ContentType] = MimeTypes.GetMimeType("file.ts"); + // TODO AllowEndOfFile = false + await new ProgressiveFileCopier(_streamHelper, state.DirectStreamProvider).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false); - return new ProgressiveFileCopier(state.DirectStreamProvider, outputHeaders, null, _logger, CancellationToken.None) - { - AllowEndOfFile = false - }; + // TODO (moved from MediaBrowser.Api): Don't hardcode contentType + return File(Response.Body, MimeTypes.GetMimeType("file.ts")!); } } // Static remote stream - if (Static && state.InputProtocol == MediaProtocol.Http) + if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http) { - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, Request, _dlnaManager); + StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager); using (state) { - return await GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false); + return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, cancellationTokenSource).ConfigureAwait(false); } } - if (Static && state.InputProtocol != MediaProtocol.File) + if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File) { - throw new ArgumentException(string.Format($"Input protocol {state.InputProtocol} cannot be streamed statically.")); + return BadRequest($"Input protocol {state.InputProtocol} cannot be streamed statically"); } var outputPath = state.OutputFilePath; - var outputPathExists = File.Exists(outputPath); + var outputPathExists = System.IO.File.Exists(outputPath); - var transcodingJob = TranscodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive); + var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive); var isTranscodeCached = outputPathExists && transcodingJob != null; - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, Static || isTranscodeCached, Request, _dlnaManager); + StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, startTimeTicks, Request, _dlnaManager); // Static stream - if (Static) + if (@static.HasValue && @static.Value) { var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath); @@ -101,16 +281,10 @@ namespace Jellyfin.Api.Controllers { if (state.MediaSource.IsInfiniteStream) { - var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - [HeaderNames.ContentType] = contentType - }; - + // TODO AllowEndOfFile = false + await new ProgressiveFileCopier(_streamHelper, state.MediaPath).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false); - return new ProgressiveFileCopier(FileSystem, state.MediaPath, outputHeaders, null, _logger, CancellationToken.None) - { - AllowEndOfFile = false - }; + return File(Response.Body, contentType); } TimeSpan? cacheDuration = null; @@ -120,57 +294,65 @@ namespace Jellyfin.Api.Controllers cacheDuration = TimeSpan.FromDays(365); } + return FileStreamResponseHelpers.GetStaticFileResult( + state.MediaPath, + contentType, + _fileSystem.GetLastWriteTimeUtc(state.MediaPath), + cacheDuration, + isHeadRequest, + this); + } + } + + /* + // Not static but transcode cache file exists + if (isTranscodeCached && state.VideoRequest == null) + { + var contentType = state.GetMimeType(outputPath) + try + { + if (transcodingJob != null) + { + ApiEntryPoint.Instance.OnTranscodeBeginRequest(transcodingJob); + } return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions { ResponseHeaders = responseHeaders, ContentType = contentType, IsHeadRequest = isHeadRequest, - Path = state.MediaPath, - CacheDuration = cacheDuration - + Path = outputPath, + FileShare = FileShare.ReadWrite, + OnComplete = () => + { + if (transcodingJob != null) + { + ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob); + } }).ConfigureAwait(false); } + finally + { + state.Dispose(); + } } + */ - //// Not static but transcode cache file exists - //if (isTranscodeCached && state.VideoRequest == null) - //{ - // var contentType = state.GetMimeType(outputPath); - - // try - // { - // if (transcodingJob != null) - // { - // ApiEntryPoint.Instance.OnTranscodeBeginRequest(transcodingJob); - // } - - // return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions - // { - // ResponseHeaders = responseHeaders, - // ContentType = contentType, - // IsHeadRequest = isHeadRequest, - // Path = outputPath, - // FileShare = FileShare.ReadWrite, - // OnComplete = () => - // { - // if (transcodingJob != null) - // { - // ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob); - // } - // } - - // }).ConfigureAwait(false); - // } - // finally - // { - // state.Dispose(); - // } - //} - - // Need to start ffmpeg + // Need to start ffmpeg (because media can't be returned directly) try { - return await GetStreamResult(request, state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false); + var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); + var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration); + var ffmpegCommandLineArguments = encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath); + return await FileStreamResponseHelpers.GetTranscodedFile( + state, + isHeadRequest, + _streamHelper, + this, + _transcodingJobHelper, + ffmpegCommandLineArguments, + Request, + _transcodingJobType, + cancellationTokenSource).ConfigureAwait(false); } catch { diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs index 05a6edf4e..da69ca72c 100644 --- a/Jellyfin.Api/Controllers/PlaystateController.cs +++ b/Jellyfin.Api/Controllers/PlaystateController.cs @@ -8,7 +8,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -40,8 +39,7 @@ namespace Jellyfin.Api.Controllers /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. + /// Th singleton. public PlaystateController( IUserManager userManager, IUserDataManager userDataRepository, @@ -49,8 +47,7 @@ namespace Jellyfin.Api.Controllers ISessionManager sessionManager, IAuthorizationContext authContext, ILoggerFactory loggerFactory, - IMediaSourceManager mediaSourceManager, - IFileSystem fileSystem) + TranscodingJobHelper transcodingJobHelper) { _userManager = userManager; _userDataRepository = userDataRepository; @@ -59,10 +56,7 @@ namespace Jellyfin.Api.Controllers _authContext = authContext; _logger = loggerFactory.CreateLogger(); - _transcodingJobHelper = new TranscodingJobHelper( - loggerFactory.CreateLogger(), - mediaSourceManager, - fileSystem); + _transcodingJobHelper = transcodingJobHelper; } /// diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs new file mode 100644 index 000000000..e03cafe35 --- /dev/null +++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs @@ -0,0 +1,236 @@ +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Api.Models.StreamingDtos; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.IO; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Jellyfin.Api.Helpers +{ + /// + /// The stream response helpers. + /// + public static class FileStreamResponseHelpers + { + /// + /// Returns a static file from a remote source. + /// + /// The current . + /// Whether the current request is a HTTP HEAD request so only the headers get returned. + /// The managing the response. + /// The . + /// A containing the API response. + public static async Task GetStaticRemoteStreamResult( + StreamState state, + bool isHeadRequest, + ControllerBase controller, + CancellationTokenSource cancellationTokenSource) + { + HttpClient httpClient = new HttpClient(); + var responseHeaders = controller.Response.Headers; + + if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent)) + { + httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, useragent); + } + + var response = await httpClient.GetAsync(state.MediaPath).ConfigureAwait(false); + var contentType = response.Content.Headers.ContentType.ToString(); + + responseHeaders[HeaderNames.AcceptRanges] = "none"; + + // Seeing cases of -1 here + if (response.Content.Headers.ContentLength.HasValue && response.Content.Headers.ContentLength.Value >= 0) + { + responseHeaders[HeaderNames.ContentLength] = response.Content.Headers.ContentLength.Value.ToString(CultureInfo.InvariantCulture); + } + + if (isHeadRequest) + { + using (response) + { + return controller.File(Array.Empty(), contentType); + } + } + + return controller.File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), contentType); + } + + /// + /// Returns a static file from the server. + /// + /// The path to the file. + /// The content type of the file. + /// The of the last modification of the file. + /// The cache duration of the file. + /// Whether the current request is a HTTP HEAD request so only the headers get returned. + /// The managing the response. + /// An the file. + // TODO: caching doesn't work + public static ActionResult GetStaticFileResult( + string path, + string contentType, + DateTime dateLastModified, + TimeSpan? cacheDuration, + bool isHeadRequest, + ControllerBase controller) + { + bool disableCaching = false; + if (controller.Request.Headers.TryGetValue(HeaderNames.CacheControl, out StringValues headerValue)) + { + disableCaching = headerValue.FirstOrDefault().Contains("no-cache", StringComparison.InvariantCulture); + } + + bool parsingSuccessful = DateTime.TryParseExact(controller.Request.Headers[HeaderNames.IfModifiedSince], "ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false), DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime ifModifiedSinceHeader); + + // if the parsing of the IfModifiedSince header was not successfull, disable caching + if (!parsingSuccessful) + { + disableCaching = true; + } + + controller.Response.ContentType = contentType; + controller.Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateLastModified).TotalSeconds).ToString(CultureInfo.InvariantCulture)); + controller.Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept); + + if (disableCaching) + { + controller.Response.Headers.Add(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate"); + controller.Response.Headers.Add(HeaderNames.Pragma, "no-cache, no-store, must-revalidate"); + } + else + { + if (cacheDuration.HasValue) + { + controller.Response.Headers.Add(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds); + } + else + { + controller.Response.Headers.Add(HeaderNames.CacheControl, "public"); + } + + controller.Response.Headers.Add(HeaderNames.LastModified, dateLastModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false))); + + // if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified + if (!(dateLastModified > ifModifiedSinceHeader)) + { + if (ifModifiedSinceHeader.Add(cacheDuration!.Value) < DateTime.UtcNow) + { + controller.Response.StatusCode = StatusCodes.Status304NotModified; + return new ContentResult(); + } + } + } + + // if the request is a head request, return a NoContent result with the same headers as it would with a GET request + if (isHeadRequest) + { + return controller.NoContent(); + } + + var stream = new FileStream(path, FileMode.Open, FileAccess.Read); + return controller.File(stream, contentType); + } + + /// + /// Returns a transcoded file from the server. + /// + /// The current . + /// Whether the current request is a HTTP HEAD request so only the headers get returned. + /// Instance of the interface. + /// The managing the response. + /// The singleton. + /// The command line arguments to start ffmpeg. + /// The starting the transcoding. + /// The . + /// The . + /// A containing the transcoded file. + public static async Task GetTranscodedFile( + StreamState state, + bool isHeadRequest, + IStreamHelper streamHelper, + ControllerBase controller, + TranscodingJobHelper transcodingJobHelper, + string ffmpegCommandLineArguments, + HttpRequest request, + TranscodingJobType transcodingJobType, + CancellationTokenSource cancellationTokenSource) + { + IHeaderDictionary responseHeaders = controller.Response.Headers; + // Use the command line args with a dummy playlist path + var outputPath = state.OutputFilePath; + + responseHeaders[HeaderNames.AcceptRanges] = "none"; + + var contentType = state.GetMimeType(outputPath); + + // TODO: The isHeadRequest is only here because ServiceStack will add Content-Length=0 to the response + // TODO (from api-migration): Investigate if this is still neccessary as we migrated away from ServiceStack + var contentLength = state.EstimateContentLength || isHeadRequest ? GetEstimatedContentLength(state) : null; + + if (contentLength.HasValue) + { + responseHeaders[HeaderNames.ContentLength] = contentLength.Value.ToString(CultureInfo.InvariantCulture); + } + else + { + responseHeaders.Remove(HeaderNames.ContentLength); + } + + // Headers only + if (isHeadRequest) + { + return controller.File(Array.Empty(), contentType); + } + + var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath); + await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); + try + { + if (!File.Exists(outputPath)) + { + await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false); + } + else + { + transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive); + state.Dispose(); + } + + Stream stream = new MemoryStream(); + + await new ProgressiveFileCopier(streamHelper, outputPath).WriteToAsync(stream, CancellationToken.None).ConfigureAwait(false); + return controller.File(stream, contentType); + } + finally + { + transcodingLock.Release(); + } + } + + /// + /// Gets the length of the estimated content. + /// + /// The state. + /// System.Nullable{System.Int64}. + private static long? GetEstimatedContentLength(StreamState state) + { + var totalBitrate = state.TotalOutputBitrate ?? 0; + + if (totalBitrate > 0 && state.RunTimeTicks.HasValue) + { + return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds / 8); + } + + return null; + } + } +} diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 4cebf40f6..c88ec0b2f 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -1,32 +1,255 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; -using Jellyfin.Api.Models; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Api.Models.StreamingDtos; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; namespace Jellyfin.Api.Helpers { /// - /// The streaming helpers + /// The streaming helpers. /// - public class StreamingHelpers + public static class StreamingHelpers { + public static async Task GetStreamingState( + Guid itemId, + long? startTimeTicks, + string? audioCodec, + string? subtitleCodec, + string? videoCodec, + string? @params, + bool? @static, + string? container, + string? liveStreamId, + string? playSessionId, + string? mediaSourceId, + string? deviceId, + string? deviceProfileId, + int? audioBitRate, + HttpRequest request, + IAuthorizationContext authorizationContext, + IMediaSourceManager mediaSourceManager, + IUserManager userManager, + ILibraryManager libraryManager, + IServerConfigurationManager serverConfigurationManager, + IMediaEncoder mediaEncoder, + IFileSystem fileSystem, + ISubtitleEncoder subtitleEncoder, + IConfiguration configuration, + IDlnaManager dlnaManager, + IDeviceManager deviceManager, + TranscodingJobHelper transcodingJobHelper, + TranscodingJobType transcodingJobType, + bool isVideoRequest, + CancellationToken cancellationToken) + { + EncodingHelper encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration); + // Parse the DLNA time seek header + if (!startTimeTicks.HasValue) + { + var timeSeek = request.Headers["TimeSeekRange.dlna.org"]; + + startTimeTicks = ParseTimeSeekHeader(timeSeek); + } + + if (!string.IsNullOrWhiteSpace(@params)) + { + // What is this? + ParseParams(request); + } + + var streamOptions = ParseStreamOptions(request.Query); + + var url = request.Path.Value.Split('.').Last(); + + if (string.IsNullOrEmpty(audioCodec)) + { + audioCodec = encodingHelper.InferAudioCodec(url); + } + + var enableDlnaHeaders = !string.IsNullOrWhiteSpace(@params) || + string.Equals(request.Headers["GetContentFeatures.DLNA.ORG"], "1", StringComparison.OrdinalIgnoreCase); + + var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper) + { + // TODO request was the StreamingRequest living in MediaBrowser.Api.Playback.Progressive + Request = request, + RequestedUrl = url, + UserAgent = request.Headers[HeaderNames.UserAgent], + EnableDlnaHeaders = enableDlnaHeaders + }; + + var auth = authorizationContext.GetAuthorizationInfo(request); + if (!auth.UserId.Equals(Guid.Empty)) + { + state.User = userManager.GetUserById(auth.UserId); + } + + /* + if ((Request.UserAgent ?? string.Empty).IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 || + (Request.UserAgent ?? string.Empty).IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 || + (Request.UserAgent ?? string.Empty).IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1) + { + state.SegmentLength = 6; + } + */ + + if (state.VideoRequest != null && !string.IsNullOrWhiteSpace(state.VideoRequest.VideoCodec)) + { + state.SupportedVideoCodecs = state.VideoRequest.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); + state.VideoRequest.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault(); + } + + if (!string.IsNullOrWhiteSpace(audioCodec)) + { + state.SupportedAudioCodecs = audioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); + state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToAudioCodec(i)) + ?? state.SupportedAudioCodecs.FirstOrDefault(); + } + + if (!string.IsNullOrWhiteSpace(subtitleCodec)) + { + state.SupportedSubtitleCodecs = subtitleCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); + state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToSubtitleCodec(i)) + ?? state.SupportedSubtitleCodecs.FirstOrDefault(); + } + + var item = libraryManager.GetItemById(itemId); + + state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase); + + /* + var primaryImage = item.GetImageInfo(ImageType.Primary, 0) ?? + item.Parents.Select(i => i.GetImageInfo(ImageType.Primary, 0)).FirstOrDefault(i => i != null); + if (primaryImage != null) + { + state.AlbumCoverPath = primaryImage.Path; + } + */ + + MediaSourceInfo? mediaSource = null; + if (string.IsNullOrWhiteSpace(liveStreamId)) + { + var currentJob = !string.IsNullOrWhiteSpace(playSessionId) + ? transcodingJobHelper.GetTranscodingJob(playSessionId) + : null; + + if (currentJob != null) + { + mediaSource = currentJob.MediaSource; + } + + if (mediaSource == null) + { + var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById(itemId), null, false, false, cancellationToken).ConfigureAwait(false); + + mediaSource = string.IsNullOrEmpty(mediaSourceId) + ? mediaSources[0] + : mediaSources.Find(i => string.Equals(i.Id, mediaSourceId, StringComparison.InvariantCulture)); + + if (mediaSource == null && Guid.Parse(mediaSourceId) == itemId) + { + mediaSource = mediaSources[0]; + } + } + } + else + { + var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(liveStreamId, cancellationToken).ConfigureAwait(false); + mediaSource = liveStreamInfo.Item1; + state.DirectStreamProvider = liveStreamInfo.Item2; + } + + encodingHelper.AttachMediaSourceInfo(state, mediaSource, url); + + var containerInternal = Path.GetExtension(state.RequestedUrl); + + if (string.IsNullOrEmpty(container)) + { + containerInternal = container; + } + + if (string.IsNullOrEmpty(containerInternal)) + { + containerInternal = (@static.HasValue && @static.Value) ? StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, state.MediaPath, null, DlnaProfileType.Audio) : GetOutputFileExtension(state); + } + + state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.'); + + state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(audioBitRate, state.AudioStream); + + state.OutputAudioCodec = audioCodec; + + state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec); + + if (isVideoRequest) + { + state.OutputVideoCodec = state.VideoRequest.VideoCodec; + state.OutputVideoBitrate = EncodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec); + + encodingHelper.TryStreamCopy(state); + + if (state.OutputVideoBitrate.HasValue && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) + { + var resolution = ResolutionNormalizer.Normalize( + state.VideoStream?.BitRate, + state.VideoStream?.Width, + state.VideoStream?.Height, + state.OutputVideoBitrate.Value, + state.VideoStream?.Codec, + state.OutputVideoCodec, + videoRequest.MaxWidth, + videoRequest.MaxHeight); + + videoRequest.MaxWidth = resolution.MaxWidth; + videoRequest.MaxHeight = resolution.MaxHeight; + } + } + + ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, request, deviceProfileId, @static); + + var ext = string.IsNullOrWhiteSpace(state.OutputContainer) + ? GetOutputFileExtension(state) + : ('.' + state.OutputContainer); + + state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, deviceId, playSessionId); + + return state; + } + /// /// Adds the dlna headers. /// /// The state. /// The response headers. /// if set to true [is statically streamed]. + /// The start time in ticks. /// The . /// Instance of the interface. public static void AddDlnaHeaders( StreamState state, IHeaderDictionary responseHeaders, bool isStaticallyStreamed, + long? startTimeTicks, HttpRequest request, IDlnaManager dlnaManager) { @@ -54,7 +277,7 @@ namespace Jellyfin.Api.Helpers if (!isStaticallyStreamed && profile != null) { - AddTimeSeekResponseHeaders(state, responseHeaders); + AddTimeSeekResponseHeaders(state, responseHeaders, startTimeTicks); } } @@ -82,51 +305,18 @@ namespace Jellyfin.Api.Helpers { var videoCodec = state.ActualOutputVideoCodec; - responseHeaders.Add("contentFeatures.dlna.org", new ContentFeatureBuilder(profile).BuildVideoHeader( - state.OutputContainer, - videoCodec, - audioCodec, - state.OutputWidth, - state.OutputHeight, - state.TargetVideoBitDepth, - state.OutputVideoBitrate, - state.TargetTimestamp, - isStaticallyStreamed, - state.RunTimeTicks, - state.TargetVideoProfile, - state.TargetVideoLevel, - state.TargetFramerate, - state.TargetPacketLength, - state.TranscodeSeekInfo, - state.IsTargetAnamorphic, - state.IsTargetInterlaced, - state.TargetRefFrames, - state.TargetVideoStreamCount, - state.TargetAudioStreamCount, - state.TargetVideoCodecTag, - state.IsTargetAVC).FirstOrDefault() ?? string.Empty); - } - } - - /// - /// Parses the dlna headers. - /// - /// The start time ticks. - /// The . - public void ParseDlnaHeaders(long? startTimeTicks, HttpRequest request) - { - if (!startTimeTicks.HasValue) - { - var timeSeek = request.Headers["TimeSeekRange.dlna.org"]; - - startTimeTicks = ParseTimeSeekHeader(timeSeek); + responseHeaders.Add( + "contentFeatures.dlna.org", + new ContentFeatureBuilder(profile).BuildVideoHeader(state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty); } } /// /// Parses the time seek header. /// - public long? ParseTimeSeekHeader(string value) + /// The time seek header string. + /// A nullable representing the seek time in ticks. + public static long? ParseTimeSeekHeader(string value) { if (string.IsNullOrWhiteSpace(value)) { @@ -138,12 +328,13 @@ namespace Jellyfin.Api.Helpers { throw new ArgumentException("Invalid timeseek header"); } - int index = value.IndexOf('-'); + + int index = value.IndexOf('-', StringComparison.InvariantCulture); value = index == -1 ? value.Substring(Npt.Length) : value.Substring(Npt.Length, index - Npt.Length); - if (value.IndexOf(':') == -1) + if (value.IndexOf(':', StringComparison.InvariantCulture) == -1) { // Parses npt times in the format of '417.33' if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds)) @@ -169,15 +360,45 @@ namespace Jellyfin.Api.Helpers { throw new ArgumentException("Invalid timeseek header"); } + timeFactor /= 60; } + return TimeSpan.FromSeconds(secondsSum).Ticks; } - public void AddTimeSeekResponseHeaders(StreamState state, IHeaderDictionary responseHeaders) + /// + /// Parses query parameters as StreamOptions. + /// + /// The query string. + /// A containing the stream options. + public static Dictionary ParseStreamOptions(IQueryCollection queryString) { - var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture); - var startSeconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture); + Dictionary streamOptions = new Dictionary(); + foreach (var param in queryString) + { + if (char.IsLower(param.Key[0])) + { + // This was probably not parsed initially and should be a StreamOptions + // or the generated URL should correctly serialize it + // TODO: This should be incorporated either in the lower framework for parsing requests + streamOptions[param.Key] = param.Value; + } + } + + return streamOptions; + } + + /// + /// Adds the dlna time seek headers to the response. + /// + /// The current . + /// The of the response. + /// The start time in ticks. + public static void AddTimeSeekResponseHeaders(StreamState state, IHeaderDictionary responseHeaders, long? startTimeTicks) + { + var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks!.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture); + var startSeconds = TimeSpan.FromTicks(startTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture); responseHeaders.Add("TimeSeekRange.dlna.org", string.Format( CultureInfo.InvariantCulture, @@ -190,5 +411,369 @@ namespace Jellyfin.Api.Helpers startSeconds, runtimeSeconds)); } + + /// + /// Gets the output file extension. + /// + /// The state. + /// System.String. + public static string? GetOutputFileExtension(StreamState state) + { + var ext = Path.GetExtension(state.RequestedUrl); + + if (!string.IsNullOrEmpty(ext)) + { + return ext; + } + + var isVideoRequest = state.VideoRequest != null; + + // Try to infer based on the desired video codec + if (isVideoRequest) + { + var videoCodec = state.VideoRequest.VideoCodec; + + if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) || + string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase)) + { + return ".ts"; + } + + if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase)) + { + return ".ogv"; + } + + if (string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase)) + { + return ".webm"; + } + + if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase)) + { + return ".asf"; + } + } + + // Try to infer based on the desired audio codec + if (!isVideoRequest) + { + var audioCodec = state.Request.AudioCodec; + + if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase)) + { + return ".aac"; + } + + if (string.Equals("mp3", audioCodec, StringComparison.OrdinalIgnoreCase)) + { + return ".mp3"; + } + + if (string.Equals("vorbis", audioCodec, StringComparison.OrdinalIgnoreCase)) + { + return ".ogg"; + } + + if (string.Equals("wma", audioCodec, StringComparison.OrdinalIgnoreCase)) + { + return ".wma"; + } + } + + return null; + } + + /// + /// Gets the output file path for transcoding. + /// + /// The current . + /// The file extension of the output file. + /// Instance of the interface. + /// The device id. + /// The play session id. + /// The complete file path, including the folder, for the transcoding file. + private static string GetOutputFilePath(StreamState state, string outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId) + { + var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}"; + + var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture); + var ext = outputFileExtension?.ToLowerInvariant(); + var folder = serverConfigurationManager.GetTranscodePath(); + + return Path.Combine(folder, filename + ext); + } + + private static void ApplyDeviceProfileSettings(StreamState state, IDlnaManager dlnaManager, IDeviceManager deviceManager, HttpRequest request, string? deviceProfileId, bool? @static) + { + var headers = request.Headers; + + if (!string.IsNullOrWhiteSpace(deviceProfileId)) + { + state.DeviceProfile = dlnaManager.GetProfile(deviceProfileId); + } + else if (!string.IsNullOrWhiteSpace(deviceProfileId)) + { + var caps = deviceManager.GetCapabilities(deviceProfileId); + + state.DeviceProfile = caps == null ? dlnaManager.GetProfile(headers) : caps.DeviceProfile; + } + + var profile = state.DeviceProfile; + + if (profile == null) + { + // Don't use settings from the default profile. + // Only use a specific profile if it was requested. + return; + } + + var audioCodec = state.ActualOutputAudioCodec; + var videoCodec = state.ActualOutputVideoCodec; + + var mediaProfile = state.VideoRequest == null + ? profile.GetAudioMediaProfile(state.OutputContainer, audioCodec, state.OutputAudioChannels, state.OutputAudioBitrate, state.OutputAudioSampleRate, state.OutputAudioBitDepth) + : profile.GetVideoMediaProfile( + state.OutputContainer, + audioCodec, + videoCodec, + state.OutputWidth, + state.OutputHeight, + state.TargetVideoBitDepth, + state.OutputVideoBitrate, + state.TargetVideoProfile, + state.TargetVideoLevel, + state.TargetFramerate, + state.TargetPacketLength, + state.TargetTimestamp, + state.IsTargetAnamorphic, + state.IsTargetInterlaced, + state.TargetRefFrames, + state.TargetVideoStreamCount, + state.TargetAudioStreamCount, + state.TargetVideoCodecTag, + state.IsTargetAVC); + + if (mediaProfile != null) + { + state.MimeType = mediaProfile.MimeType; + } + + if (!(@static.HasValue && @static.Value)) + { + var transcodingProfile = state.VideoRequest == null ? profile.GetAudioTranscodingProfile(state.OutputContainer, audioCodec) : profile.GetVideoTranscodingProfile(state.OutputContainer, audioCodec, videoCodec); + + if (transcodingProfile != null) + { + state.EstimateContentLength = transcodingProfile.EstimateContentLength; + // state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode; + state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo; + + if (state.VideoRequest != null) + { + state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps; + state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest; + } + } + } + } + + /// + /// Parses the parameters. + /// + /// The request. + private void ParseParams(StreamRequest request) + { + var vals = request.Params.Split(';'); + + var videoRequest = request as VideoStreamRequest; + + for (var i = 0; i < vals.Length; i++) + { + var val = vals[i]; + + if (string.IsNullOrWhiteSpace(val)) + { + continue; + } + + switch (i) + { + case 0: + request.DeviceProfileId = val; + break; + case 1: + request.DeviceId = val; + break; + case 2: + request.MediaSourceId = val; + break; + case 3: + request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); + break; + case 4: + if (videoRequest != null) + { + videoRequest.VideoCodec = val; + } + + break; + case 5: + request.AudioCodec = val; + break; + case 6: + if (videoRequest != null) + { + videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 7: + if (videoRequest != null) + { + videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 8: + if (videoRequest != null) + { + videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 9: + request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture); + break; + case 10: + request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture); + break; + case 11: + if (videoRequest != null) + { + videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 12: + if (videoRequest != null) + { + videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 13: + if (videoRequest != null) + { + videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 14: + request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture); + break; + case 15: + if (videoRequest != null) + { + videoRequest.Level = val; + } + + break; + case 16: + if (videoRequest != null) + { + videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 17: + if (videoRequest != null) + { + videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 18: + if (videoRequest != null) + { + videoRequest.Profile = val; + } + + break; + case 19: + // cabac no longer used + break; + case 20: + request.PlaySessionId = val; + break; + case 21: + // api_key + break; + case 22: + request.LiveStreamId = val; + break; + case 23: + // Duplicating ItemId because of MediaMonkey + break; + case 24: + if (videoRequest != null) + { + videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); + } + + break; + case 25: + if (!string.IsNullOrWhiteSpace(val) && videoRequest != null) + { + if (Enum.TryParse(val, out SubtitleDeliveryMethod method)) + { + videoRequest.SubtitleMethod = method; + } + } + + break; + case 26: + request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture); + break; + case 27: + if (videoRequest != null) + { + videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); + } + + break; + case 28: + request.Tag = val; + break; + case 29: + if (videoRequest != null) + { + videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); + } + + break; + case 30: + request.SubtitleCodec = val; + break; + case 31: + if (videoRequest != null) + { + videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); + } + + break; + case 32: + if (videoRequest != null) + { + videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); + } + + break; + case 33: + request.TranscodeReasons = val; + break; + } + } + } } } diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 7db75387a..9fbd5ec2d 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -1,16 +1,28 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; +using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Api.Models; using Jellyfin.Api.Models.PlaybackDtos; +using Jellyfin.Api.Models.StreamingDtos; +using Jellyfin.Data.Enums; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; +using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Session; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Helpers @@ -30,9 +42,17 @@ namespace Jellyfin.Api.Helpers /// private static readonly Dictionary _transcodingLocks = new Dictionary(); + private readonly IAuthorizationContext _authorizationContext; + private readonly EncodingHelper _encodingHelper; + private readonly IFileSystem _fileSystem; + private readonly IIsoManager _isoManager; + private readonly ILogger _logger; + private readonly IMediaEncoder _mediaEncoder; private readonly IMediaSourceManager _mediaSourceManager; - private readonly IFileSystem _fileSystem; + private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly ISessionManager _sessionManager; + private readonly ILoggerFactory _loggerFactory; /// /// Initializes a new instance of the class. @@ -40,14 +60,40 @@ namespace Jellyfin.Api.Helpers /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. public TranscodingJobHelper( ILogger logger, IMediaSourceManager mediaSourceManager, - IFileSystem fileSystem) + IFileSystem fileSystem, + IMediaEncoder mediaEncoder, + IServerConfigurationManager serverConfigurationManager, + ISessionManager sessionManager, + IAuthorizationContext authorizationContext, + IIsoManager isoManager, + ISubtitleEncoder subtitleEncoder, + IConfiguration configuration, + ILoggerFactory loggerFactory) { _logger = logger; _mediaSourceManager = mediaSourceManager; _fileSystem = fileSystem; + _mediaEncoder = mediaEncoder; + _serverConfigurationManager = serverConfigurationManager; + _sessionManager = sessionManager; + _authorizationContext = authorizationContext; + _isoManager = isoManager; + _loggerFactory = loggerFactory; + + _encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration); + + DeleteEncodedMediaCache(); } /// @@ -63,7 +109,13 @@ namespace Jellyfin.Api.Helpers } } - public static TranscodingJobDto GetTranscodingJob(string path, TranscodingJobType type) + /// + /// Get transcoding job. + /// + /// Path to the transcoding file. + /// The . + /// The transcoding job. + public TranscodingJobDto GetTranscodingJob(string path, TranscodingJobType type) { lock (_activeTranscodingJobs) { @@ -361,14 +413,24 @@ namespace Jellyfin.Api.Helpers } } + /// + /// Report the transcoding progress to the session manager. + /// + /// The of which the progress will be reported. + /// The of the current transcoding job. + /// The current transcoding position. + /// The framerate of the transcoding job. + /// The completion percentage of the transcode. + /// The number of bytes transcoded. + /// The bitrate of the transcoding job. public void ReportTranscodingProgress( - TranscodingJob job, - StreamState state, - TimeSpan? transcodingPosition, - float? framerate, - double? percentComplete, - long? bytesTranscoded, - int? bitRate) + TranscodingJobDto job, + StreamState state, + TimeSpan? transcodingPosition, + float? framerate, + double? percentComplete, + long? bytesTranscoded, + int? bitRate) { var ticks = transcodingPosition?.Ticks; @@ -405,5 +467,374 @@ namespace Jellyfin.Api.Helpers }); } } + + /// + /// Starts the FFMPEG. + /// + /// The state. + /// The output path. + /// The command line arguments for ffmpeg. + /// The . + /// The . + /// The cancellation token source. + /// The working directory. + /// Task. + public async Task StartFfMpeg( + StreamState state, + string outputPath, + string commandLineArguments, + HttpRequest request, + TranscodingJobType transcodingJobType, + CancellationTokenSource cancellationTokenSource, + string workingDirectory = null) + { + Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); + + await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false); + + if (state.VideoRequest != null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) + { + var auth = _authorizationContext.GetAuthorizationInfo(request); + if (auth.User != null && !auth.User.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)) + { + this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state); + + throw new ArgumentException("User does not have access to video transcoding"); + } + } + + var process = new Process() + { + StartInfo = new ProcessStartInfo() + { + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + UseShellExecute = false, + + // Must consume both stdout and stderr or deadlocks may occur + // RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = true, + FileName = _mediaEncoder.EncoderPath, + Arguments = commandLineArguments, + WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? null : workingDirectory, + ErrorDialog = false + }, + EnableRaisingEvents = true + }; + + var transcodingJob = this.OnTranscodeBeginning( + outputPath, + state.Request.PlaySessionId, + state.MediaSource.LiveStreamId, + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), + transcodingJobType, + process, + state.Request.DeviceId, + state, + cancellationTokenSource); + + var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments; + _logger.LogInformation(commandLineLogMessage); + + var logFilePrefix = "ffmpeg-transcode"; + if (state.VideoRequest != null + && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) + { + logFilePrefix = EncodingHelper.IsCopyCodec(state.OutputAudioCodec) + ? "ffmpeg-remux" + : "ffmpeg-directstream"; + } + + var logFilePath = Path.Combine(_serverConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt"); + + // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. + Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); + + var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine); + await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false); + + process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state); + + try + { + process.Start(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error starting ffmpeg"); + + this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state); + + throw; + } + + _logger.LogDebug("Launched ffmpeg process"); + state.TranscodingJob = transcodingJob; + + // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback + _ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream); + + // Wait for the file to exist before proceeeding + var ffmpegTargetFile = state.WaitForPath ?? outputPath; + _logger.LogDebug("Waiting for the creation of {0}", ffmpegTargetFile); + while (!File.Exists(ffmpegTargetFile) && !transcodingJob.HasExited) + { + await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false); + } + + _logger.LogDebug("File {0} created or transcoding has finished", ffmpegTargetFile); + + if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited) + { + await Task.Delay(1000, cancellationTokenSource.Token).ConfigureAwait(false); + + if (state.ReadInputAtNativeFramerate && !transcodingJob.HasExited) + { + await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false); + } + } + + if (!transcodingJob.HasExited) + { + StartThrottler(state, transcodingJob); + } + + _logger.LogDebug("StartFfMpeg() finished successfully"); + + return transcodingJob; + } + + private void StartThrottler(StreamState state, TranscodingJobDto transcodingJob) + { + if (EnableThrottling(state)) + { + transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger(new LoggerFactory()), _serverConfigurationManager, _fileSystem); + state.TranscodingThrottler.Start(); + } + } + + private bool EnableThrottling(StreamState state) + { + var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); + + // enable throttling when NOT using hardware acceleration + if (string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType)) + { + return state.InputProtocol == MediaProtocol.File && + state.RunTimeTicks.HasValue && + state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && + state.IsInputVideo && + state.VideoType == VideoType.VideoFile && + !EncodingHelper.IsCopyCodec(state.OutputVideoCodec); + } + + return false; + } + + /// + /// Called when [transcode beginning]. + /// + /// The path. + /// The play session identifier. + /// The live stream identifier. + /// The transcoding job identifier. + /// The type. + /// The process. + /// The device id. + /// The state. + /// The cancellation token source. + /// TranscodingJob. + public TranscodingJobDto OnTranscodeBeginning( + string path, + string playSessionId, + string liveStreamId, + string transcodingJobId, + TranscodingJobType type, + Process process, + string deviceId, + StreamState state, + CancellationTokenSource cancellationTokenSource) + { + lock (_activeTranscodingJobs) + { + var job = new TranscodingJobDto(_loggerFactory.CreateLogger()) + { + Type = type, + Path = path, + Process = process, + ActiveRequestCount = 1, + DeviceId = deviceId, + CancellationTokenSource = cancellationTokenSource, + Id = transcodingJobId, + PlaySessionId = playSessionId, + LiveStreamId = liveStreamId, + MediaSource = state.MediaSource + }; + + _activeTranscodingJobs.Add(job); + + ReportTranscodingProgress(job, state, null, null, null, null, null); + + return job; + } + } + + /// + /// + /// The progressive + /// + /// Called when [transcode failed to start]. + /// + /// The path. + /// The type. + /// The state. + public void OnTranscodeFailedToStart(string path, TranscodingJobType type, StreamState state) + { + lock (_activeTranscodingJobs) + { + var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase)); + + if (job != null) + { + _activeTranscodingJobs.Remove(job); + } + } + + lock (_transcodingLocks) + { + _transcodingLocks.Remove(path); + } + + if (!string.IsNullOrWhiteSpace(state.Request.DeviceId)) + { + _sessionManager.ClearTranscodingInfo(state.Request.DeviceId); + } + } + + /// + /// Processes the exited. + /// + /// The process. + /// The job. + /// The state. + private void OnFfMpegProcessExited(Process process, TranscodingJobDto job, StreamState state) + { + if (job != null) + { + job.HasExited = true; + } + + _logger.LogDebug("Disposing stream resources"); + state.Dispose(); + + if (process.ExitCode == 0) + { + _logger.LogInformation("FFMpeg exited with code 0"); + } + else + { + _logger.LogError("FFMpeg exited with code {0}", process.ExitCode); + } + + process.Dispose(); + } + + private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource) + { + if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && _isoManager.CanMount(state.MediaPath)) + { + state.IsoMount = await _isoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false); + } + + if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId)) + { + var liveStreamResponse = await _mediaSourceManager.OpenLiveStream( + new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken }, + cancellationTokenSource.Token) + .ConfigureAwait(false); + + _encodingHelper.AttachMediaSourceInfo(state, liveStreamResponse.MediaSource, state.RequestedUrl); + + if (state.VideoRequest != null) + { + _encodingHelper.TryStreamCopy(state); + } + } + + if (state.MediaSource.BufferMs.HasValue) + { + await Task.Delay(state.MediaSource.BufferMs.Value, cancellationTokenSource.Token).ConfigureAwait(false); + } + } + + /// + /// Called when [transcode begin request]. + /// + /// The path. + /// The type. + /// The . + public TranscodingJobDto? OnTranscodeBeginRequest(string path, TranscodingJobType type) + { + lock (_activeTranscodingJobs) + { + var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase)); + + if (job == null) + { + return null; + } + + OnTranscodeBeginRequest(job); + + return job; + } + } + + private void OnTranscodeBeginRequest(TranscodingJobDto job) + { + job.ActiveRequestCount++; + + if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive) + { + job.StopKillTimer(); + } + } + + /// + /// Gets the transcoding lock. + /// + /// The output path of the transcoded file. + /// A . + public SemaphoreSlim GetTranscodingLock(string outputPath) + { + lock (_transcodingLocks) + { + if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim result)) + { + result = new SemaphoreSlim(1, 1); + _transcodingLocks[outputPath] = result; + } + + return result; + } + } + + /// + /// Deletes the encoded media cache. + /// + private void DeleteEncodedMediaCache() + { + var path = _serverConfigurationManager.GetTranscodePath(); + if (!Directory.Exists(path)) + { + return; + } + + foreach (var file in _fileSystem.GetFilePaths(path, true)) + { + _fileSystem.DeleteFile(file); + } + } } } diff --git a/Jellyfin.Api/Models/StreamState.cs b/Jellyfin.Api/Models/StreamState.cs deleted file mode 100644 index 9fe5f52c3..000000000 --- a/Jellyfin.Api/Models/StreamState.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System; -using Jellyfin.Api.Helpers; -using Jellyfin.Api.Models.PlaybackDtos; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Model.Dlna; - -namespace Jellyfin.Api.Models -{ - public class StreamState : EncodingJobInfo, IDisposable - { - private readonly IMediaSourceManager _mediaSourceManager; - private bool _disposed = false; - - public string RequestedUrl { get; set; } - - public StreamRequest Request - { - get => (StreamRequest)BaseRequest; - set - { - BaseRequest = value; - - IsVideoRequest = VideoRequest != null; - } - } - - public TranscodingThrottler TranscodingThrottler { get; set; } - - public VideoStreamRequest VideoRequest => Request as VideoStreamRequest; - - public IDirectStreamProvider DirectStreamProvider { get; set; } - - public string WaitForPath { get; set; } - - public bool IsOutputVideo => Request is VideoStreamRequest; - - public int SegmentLength - { - get - { - if (Request.SegmentLength.HasValue) - { - return Request.SegmentLength.Value; - } - - if (EncodingHelper.IsCopyCodec(OutputVideoCodec)) - { - var userAgent = UserAgent ?? string.Empty; - - if (userAgent.IndexOf("AppleTV", StringComparison.OrdinalIgnoreCase) != -1 || - userAgent.IndexOf("cfnetwork", StringComparison.OrdinalIgnoreCase) != -1 || - userAgent.IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 || - userAgent.IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 || - userAgent.IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1) - { - if (IsSegmentedLiveStream) - { - return 6; - } - - return 6; - } - - if (IsSegmentedLiveStream) - { - return 3; - } - - return 6; - } - - return 3; - } - } - - public int MinSegments - { - get - { - if (Request.MinSegments.HasValue) - { - return Request.MinSegments.Value; - } - - return SegmentLength >= 10 ? 2 : 3; - } - } - - public string UserAgent { get; set; } - - public bool EstimateContentLength { get; set; } - - public TranscodeSeekInfo TranscodeSeekInfo { get; set; } - - public bool EnableDlnaHeaders { get; set; } - - public DeviceProfile DeviceProfile { get; set; } - - public TranscodingJobDto TranscodingJob { get; set; } - - public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType) - : base(transcodingType) - { - _mediaSourceManager = mediaSourceManager; - } - - public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate) - { - TranscodingJobHelper.ReportTranscodingProgress(TranscodingJob, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - // REVIEW: Is this the right place for this? - if (MediaSource.RequiresClosing - && string.IsNullOrWhiteSpace(Request.LiveStreamId) - && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId)) - { - _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult(); - } - - TranscodingThrottler?.Dispose(); - } - - TranscodingThrottler = null; - TranscodingJob = null; - - _disposed = true; - } - } -} diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs new file mode 100644 index 000000000..b962e0ac7 --- /dev/null +++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs @@ -0,0 +1,207 @@ +using System; +using Jellyfin.Api.Helpers; +using Jellyfin.Api.Models.PlaybackDtos; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Dlna; + +namespace Jellyfin.Api.Models.StreamingDtos +{ + /// + /// The stream state dto. + /// + public class StreamState : EncodingJobInfo, IDisposable + { + private readonly IMediaSourceManager _mediaSourceManager; + private readonly TranscodingJobHelper _transcodingJobHelper; + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// The . + /// The singleton. + public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType, TranscodingJobHelper transcodingJobHelper) + : base(transcodingType) + { + _mediaSourceManager = mediaSourceManager; + _transcodingJobHelper = transcodingJobHelper; + } + + /// + /// Gets or sets the requested url. + /// + public string? RequestedUrl { get; set; } + + // /// + // /// Gets or sets the request. + // /// + // public StreamRequest Request + // { + // get => (StreamRequest)BaseRequest; + // set + // { + // BaseRequest = value; + // + // IsVideoRequest = VideoRequest != null; + // } + // } + + /// + /// Gets or sets the transcoding throttler. + /// + public TranscodingThrottler? TranscodingThrottler { get; set; } + + /// + /// Gets the video request. + /// + public VideoStreamRequest VideoRequest => Request as VideoStreamRequest; + + /// + /// Gets or sets the direct stream provicer. + /// + public IDirectStreamProvider? DirectStreamProvider { get; set; } + + /// + /// Gets or sets the path to wait for. + /// + public string? WaitForPath { get; set; } + + /// + /// Gets a value indicating whether the request outputs video. + /// + public bool IsOutputVideo => Request is VideoStreamRequest; + + /// + /// Gets the segment length. + /// + public int SegmentLength + { + get + { + if (Request.SegmentLength.HasValue) + { + return Request.SegmentLength.Value; + } + + if (EncodingHelper.IsCopyCodec(OutputVideoCodec)) + { + var userAgent = UserAgent ?? string.Empty; + + if (userAgent.IndexOf("AppleTV", StringComparison.OrdinalIgnoreCase) != -1 || + userAgent.IndexOf("cfnetwork", StringComparison.OrdinalIgnoreCase) != -1 || + userAgent.IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 || + userAgent.IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 || + userAgent.IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1) + { + if (IsSegmentedLiveStream) + { + return 6; + } + + return 6; + } + + if (IsSegmentedLiveStream) + { + return 3; + } + + return 6; + } + + return 3; + } + } + + /// + /// Gets the minimum number of segments. + /// + public int MinSegments + { + get + { + if (Request.MinSegments.HasValue) + { + return Request.MinSegments.Value; + } + + return SegmentLength >= 10 ? 2 : 3; + } + } + + /// + /// Gets or sets the user agent. + /// + public string? UserAgent { get; set; } + + /// + /// Gets or sets a value indicating whether to estimate the content length. + /// + public bool EstimateContentLength { get; set; } + + /// + /// Gets or sets the transcode seek info. + /// + public TranscodeSeekInfo TranscodeSeekInfo { get; set; } + + /// + /// Gets or sets a value indicating whether to enable dlna headers. + /// + public bool EnableDlnaHeaders { get; set; } + + /// + /// Gets or sets the device profile. + /// + public DeviceProfile? DeviceProfile { get; set; } + + /// + /// Gets or sets the transcoding job. + /// + public TranscodingJobDto? TranscodingJob { get; set; } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate) + { + _transcodingJobHelper.ReportTranscodingProgress(TranscodingJob!, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate); + } + + /// + /// Disposes the stream state. + /// + /// Whether the object is currently beeing disposed. + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + // REVIEW: Is this the right place for this? + if (MediaSource.RequiresClosing + && string.IsNullOrWhiteSpace(Request.LiveStreamId) + && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId)) + { + _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult(); + } + + TranscodingThrottler?.Dispose(); + } + + TranscodingThrottler = null; + TranscodingJob = null; + + _disposed = true; + } + } +} diff --git a/MediaBrowser.Api/Playback/Progressive/AudioService.cs b/MediaBrowser.Api/Playback/Progressive/AudioService.cs index 34c7986ca..ef639851b 100644 --- a/MediaBrowser.Api/Playback/Progressive/AudioService.cs +++ b/MediaBrowser.Api/Playback/Progressive/AudioService.cs @@ -17,10 +17,6 @@ namespace MediaBrowser.Api.Playback.Progressive /// /// Class GetAudioStream /// - [Route("/Audio/{Id}/stream.{Container}", "GET", Summary = "Gets an audio stream")] - [Route("/Audio/{Id}/stream", "GET", Summary = "Gets an audio stream")] - [Route("/Audio/{Id}/stream.{Container}", "HEAD", Summary = "Gets an audio stream")] - [Route("/Audio/{Id}/stream", "HEAD", Summary = "Gets an audio stream")] public class GetAudioStream : StreamRequest { } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 8ce106469..8cfe562b3 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1263,6 +1263,17 @@ namespace MediaBrowser.Controller.MediaEncoding return null; } + public int? GetAudioBitrateParam(int? audioBitRate, MediaStream audioStream) + { + if (audioBitRate.HasValue) + { + // Don't encode any higher than this + return Math.Min(384000, audioBitRate.Value); + } + + return null; + } + public string GetAudioFilterParam(EncodingJobInfo state, EncodingOptions encodingOptions, bool isHls) { var channels = state.OutputAudioChannels; -- cgit v1.2.3 From 5580df38e62ba75762da2f2b3ed4acd69b66e391 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 22 Jul 2020 11:05:41 +0200 Subject: Cleanup after merge --- Emby.Server.Implementations/ApplicationHost.cs | 2 -- 1 file changed, 2 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 34a4cc575..ad6cbe167 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.IO; using System.Linq; using System.Net; @@ -46,7 +45,6 @@ using Emby.Server.Implementations.Session; using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; -using Emby.Server.Implementations.SyncPlay; using Jellyfin.Api.Helpers; using MediaBrowser.Api; using MediaBrowser.Common; -- cgit v1.2.3 From 200f36959638187a220ad152d42333a787a83f8e Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Mon, 29 Jun 2020 23:00:46 -0400 Subject: Use interfaces in app host constructors --- Emby.Server.Implementations/ApplicationHost.cs | 4 ++-- Jellyfin.Server/CoreAppHost.cs | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index f6077400d..8e1bf9766 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -192,7 +192,7 @@ namespace Emby.Server.Implementations /// Gets or sets the application paths. /// /// The application paths. - protected ServerApplicationPaths ApplicationPaths { get; set; } + protected IServerApplicationPaths ApplicationPaths { get; set; } /// /// Gets or sets all concrete types. @@ -236,7 +236,7 @@ namespace Emby.Server.Implementations /// Initializes a new instance of the class. /// public ApplicationHost( - ServerApplicationPaths applicationPaths, + IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IStartupOptions options, IFileSystem fileSystem, diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 207eaa98d..71b0fd8f3 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -9,6 +9,7 @@ using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Activity; using Jellyfin.Server.Implementations.Users; using MediaBrowser.Common.Net; +using MediaBrowser.Controller; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Activity; @@ -33,9 +34,9 @@ namespace Jellyfin.Server /// The to be used by the . /// The to be used by the . public CoreAppHost( - ServerApplicationPaths applicationPaths, + IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, - StartupOptions options, + IStartupOptions options, IFileSystem fileSystem, INetworkManager networkManager) : base( -- cgit v1.2.3 From dee7bdddb6f5ce0cc09dc2b20d4dab9747eea9f0 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 3 Aug 2020 14:49:24 -0600 Subject: fix build --- Emby.Server.Implementations/ApplicationHost.cs | 4 ---- 1 file changed, 4 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 7647827fb..0201ed7a3 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -46,7 +46,6 @@ using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; using Jellyfin.Api.Helpers; -using MediaBrowser.Api; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; @@ -1032,9 +1031,6 @@ namespace Emby.Server.Implementations } } - // Include composable parts in the Api assembly - yield return typeof(ApiEntryPoint).Assembly; - // Include composable parts in the Model assembly yield return typeof(SystemInfo).Assembly; -- cgit v1.2.3 From 460c3dd35166c9a48db83db62a3f0a8742956408 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 9 Aug 2020 17:20:14 -0600 Subject: convert dependent controller functions to di helper class --- Emby.Server.Implementations/ApplicationHost.cs | 3 + Jellyfin.Api/Controllers/AudioController.cs | 167 +----- Jellyfin.Api/Controllers/DynamicHlsController.cs | 446 +--------------- Jellyfin.Api/Controllers/MediaInfoController.cs | 538 ++----------------- .../Controllers/UniversalAudioController.cs | 280 +++++----- Jellyfin.Api/Helpers/AudioHelper.cs | 193 +++++++ Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 549 ++++++++++++++++++++ Jellyfin.Api/Helpers/MediaInfoHelper.cs | 573 +++++++++++++++++++++ 8 files changed, 1510 insertions(+), 1239 deletions(-) create mode 100644 Jellyfin.Api/Helpers/AudioHelper.cs create mode 100644 Jellyfin.Api/Helpers/DynamicHlsHelper.cs create mode 100644 Jellyfin.Api/Helpers/MediaInfoHelper.cs (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 0201ed7a3..99530bfbd 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -632,6 +632,9 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddScoped(); + serviceCollection.AddScoped(); + serviceCollection.AddScoped(); } /// diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 4de87616c..107db9571 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -1,93 +1,32 @@ using System; using System.Collections.Generic; -using System.Net.Http; -using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.StreamingDtos; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; namespace Jellyfin.Api.Controllers { /// /// The audio controller. /// - // TODO: In order to autheneticate this in the future, Dlna playback will require updating + // TODO: In order to authenticate this in the future, Dlna playback will require updating public class AudioController : BaseJellyfinApiController { - private readonly IDlnaManager _dlnaManager; - private readonly IAuthorizationContext _authContext; - private readonly IUserManager _userManager; - private readonly ILibraryManager _libraryManager; - private readonly IMediaSourceManager _mediaSourceManager; - private readonly IServerConfigurationManager _serverConfigurationManager; - private readonly IMediaEncoder _mediaEncoder; - private readonly IFileSystem _fileSystem; - private readonly ISubtitleEncoder _subtitleEncoder; - private readonly IConfiguration _configuration; - private readonly IDeviceManager _deviceManager; - private readonly TranscodingJobHelper _transcodingJobHelper; - private readonly IHttpClientFactory _httpClientFactory; + private readonly AudioHelper _audioHelper; private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive; /// /// Initializes a new instance of the class. /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// The singleton. - /// Instance of the interface. - public AudioController( - IDlnaManager dlnaManager, - IUserManager userManger, - IAuthorizationContext authorizationContext, - ILibraryManager libraryManager, - IMediaSourceManager mediaSourceManager, - IServerConfigurationManager serverConfigurationManager, - IMediaEncoder mediaEncoder, - IFileSystem fileSystem, - ISubtitleEncoder subtitleEncoder, - IConfiguration configuration, - IDeviceManager deviceManager, - TranscodingJobHelper transcodingJobHelper, - IHttpClientFactory httpClientFactory) + /// Instance of . + public AudioController(AudioHelper audioHelper) { - _dlnaManager = dlnaManager; - _authContext = authorizationContext; - _userManager = userManger; - _libraryManager = libraryManager; - _mediaSourceManager = mediaSourceManager; - _serverConfigurationManager = serverConfigurationManager; - _mediaEncoder = mediaEncoder; - _fileSystem = fileSystem; - _subtitleEncoder = subtitleEncoder; - _configuration = configuration; - _deviceManager = deviceManager; - _transcodingJobHelper = transcodingJobHelper; - _httpClientFactory = httpClientFactory; + _audioHelper = audioHelper; } /// @@ -200,10 +139,6 @@ namespace Jellyfin.Api.Controllers [FromQuery] EncodingContext? context, [FromQuery] Dictionary? streamOptions) { - bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head; - - var cancellationTokenSource = new CancellationTokenSource(); - StreamingRequestDto streamingRequest = new StreamingRequestDto { Id = itemId, @@ -257,97 +192,7 @@ namespace Jellyfin.Api.Controllers StreamOptions = streamOptions }; - using var state = await StreamingHelpers.GetStreamingState( - streamingRequest, - Request, - _authContext, - _mediaSourceManager, - _userManager, - _libraryManager, - _serverConfigurationManager, - _mediaEncoder, - _fileSystem, - _subtitleEncoder, - _configuration, - _dlnaManager, - _deviceManager, - _transcodingJobHelper, - _transcodingJobType, - cancellationTokenSource.Token) - .ConfigureAwait(false); - - if (@static.HasValue && @static.Value && state.DirectStreamProvider != null) - { - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager); - - await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None) - { - AllowEndOfFile = false - }.WriteToAsync(Response.Body, CancellationToken.None) - .ConfigureAwait(false); - - // TODO (moved from MediaBrowser.Api): Don't hardcode contentType - return File(Response.Body, MimeTypes.GetMimeType("file.ts")!); - } - - // Static remote stream - if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http) - { - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager); - - using var httpClient = _httpClientFactory.CreateClient(); - return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, httpClient).ConfigureAwait(false); - } - - if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File) - { - return BadRequest($"Input protocol {state.InputProtocol} cannot be streamed statically"); - } - - var outputPath = state.OutputFilePath; - var outputPathExists = System.IO.File.Exists(outputPath); - - var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive); - var isTranscodeCached = outputPathExists && transcodingJob != null; - - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, startTimeTicks, Request, _dlnaManager); - - // Static stream - if (@static.HasValue && @static.Value) - { - var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath); - - if (state.MediaSource.IsInfiniteStream) - { - await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None) - { - AllowEndOfFile = false - }.WriteToAsync(Response.Body, CancellationToken.None) - .ConfigureAwait(false); - - return File(Response.Body, contentType); - } - - return FileStreamResponseHelpers.GetStaticFileResult( - state.MediaPath, - contentType, - isHeadRequest, - this); - } - - // Need to start ffmpeg (because media can't be returned directly) - var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); - var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration); - var ffmpegCommandLineArguments = encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath); - return await FileStreamResponseHelpers.GetTranscodedFile( - state, - isHeadRequest, - this, - _transcodingJobHelper, - ffmpegCommandLineArguments, - Request, - _transcodingJobType, - cancellationTokenSource).ConfigureAwait(false); + return await _audioHelper.GetAudioStream(this, _transcodingJobType, streamingRequest).ConfigureAwait(false); } } } diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index d581ab8cd..1a85a07b8 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -13,7 +13,6 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; @@ -22,7 +21,6 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Authorization; @@ -53,9 +51,9 @@ namespace Jellyfin.Api.Controllers private readonly IConfiguration _configuration; private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; - private readonly INetworkManager _networkManager; private readonly ILogger _logger; private readonly EncodingHelper _encodingHelper; + private readonly DynamicHlsHelper _dynamicHlsHelper; private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls; @@ -74,8 +72,8 @@ namespace Jellyfin.Api.Controllers /// Instance of the interface. /// Instance of the interface. /// Instance of the class. - /// Instance of the interface. /// Instance of the interface. + /// Instance of . public DynamicHlsController( ILibraryManager libraryManager, IUserManager userManager, @@ -89,8 +87,8 @@ namespace Jellyfin.Api.Controllers IConfiguration configuration, IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, - INetworkManager networkManager, - ILogger logger) + ILogger logger, + DynamicHlsHelper dynamicHlsHelper) { _libraryManager = libraryManager; _userManager = userManager; @@ -104,8 +102,8 @@ namespace Jellyfin.Api.Controllers _configuration = configuration; _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; - _networkManager = networkManager; _logger = logger; + _dynamicHlsHelper = dynamicHlsHelper; _encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration); } @@ -220,8 +218,6 @@ namespace Jellyfin.Api.Controllers [FromQuery] Dictionary streamOptions, [FromQuery] bool enableAdaptiveBitrateStreaming = true) { - var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head; - var cancellationTokenSource = new CancellationTokenSource(); var streamingRequest = new HlsVideoRequestDto { Id = itemId, @@ -276,8 +272,7 @@ namespace Jellyfin.Api.Controllers EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming }; - return await GetMasterPlaylistInternal(streamingRequest, isHeadRequest, enableAdaptiveBitrateStreaming, cancellationTokenSource) - .ConfigureAwait(false); + return await _dynamicHlsHelper.GetMasterHlsPlaylist(this, _transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); } /// @@ -390,8 +385,6 @@ namespace Jellyfin.Api.Controllers [FromQuery] Dictionary streamOptions, [FromQuery] bool enableAdaptiveBitrateStreaming = true) { - var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head; - var cancellationTokenSource = new CancellationTokenSource(); var streamingRequest = new HlsAudioRequestDto { Id = itemId, @@ -446,8 +439,7 @@ namespace Jellyfin.Api.Controllers EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming }; - return await GetMasterPlaylistInternal(streamingRequest, isHeadRequest, enableAdaptiveBitrateStreaming, cancellationTokenSource) - .ConfigureAwait(false); + return await _dynamicHlsHelper.GetMasterHlsPlaylist(this, _transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); } /// @@ -1118,106 +1110,6 @@ namespace Jellyfin.Api.Controllers .ConfigureAwait(false); } - private async Task GetMasterPlaylistInternal( - StreamingRequestDto streamingRequest, - bool isHeadRequest, - bool enableAdaptiveBitrateStreaming, - CancellationTokenSource cancellationTokenSource) - { - using var state = await StreamingHelpers.GetStreamingState( - streamingRequest, - Request, - _authContext, - _mediaSourceManager, - _userManager, - _libraryManager, - _serverConfigurationManager, - _mediaEncoder, - _fileSystem, - _subtitleEncoder, - _configuration, - _dlnaManager, - _deviceManager, - _transcodingJobHelper, - _transcodingJobType, - cancellationTokenSource.Token) - .ConfigureAwait(false); - - Response.Headers.Add(HeaderNames.Expires, "0"); - if (isHeadRequest) - { - return new FileContentResult(Array.Empty(), MimeTypes.GetMimeType("playlist.m3u8")); - } - - var totalBitrate = state.OutputAudioBitrate ?? 0 + state.OutputVideoBitrate ?? 0; - - var builder = new StringBuilder(); - - builder.AppendLine("#EXTM3U"); - - var isLiveStream = state.IsSegmentedLiveStream; - - var queryString = Request.QueryString.ToString(); - - // from universal audio service - if (queryString.IndexOf("SegmentContainer", StringComparison.OrdinalIgnoreCase) == -1 && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer)) - { - queryString += "&SegmentContainer=" + state.Request.SegmentContainer; - } - - // from universal audio service - if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons) && queryString.IndexOf("TranscodeReasons=", StringComparison.OrdinalIgnoreCase) == -1) - { - queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons; - } - - // Main stream - var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8"; - - playlistUrl += queryString; - - var subtitleStreams = state.MediaSource - .MediaStreams - .Where(i => i.IsTextSubtitleStream) - .ToList(); - - var subtitleGroup = subtitleStreams.Count > 0 && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest!.EnableSubtitlesInManifest) - ? "subs" - : null; - - // If we're burning in subtitles then don't add additional subs to the manifest - if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) - { - subtitleGroup = null; - } - - if (!string.IsNullOrWhiteSpace(subtitleGroup)) - { - AddSubtitles(state, subtitleStreams, builder); - } - - AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); - - if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming)) - { - var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0; - - // By default, vary by just 200k - var variation = GetBitrateVariation(totalBitrate); - - var newBitrate = totalBitrate - variation; - var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); - AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); - - variation *= 2; - newBitrate = totalBitrate - variation; - variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); - AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); - } - - return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); - } - private async Task GetVariantPlaylistInternal(StreamingRequestDto streamingRequest, string name, CancellationTokenSource cancellationTokenSource) { using var state = await StreamingHelpers.GetStreamingState( @@ -1411,330 +1303,6 @@ namespace Jellyfin.Api.Controllers return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false); } - private void AddSubtitles(StreamState state, IEnumerable subtitles, StringBuilder builder) - { - var selectedIndex = state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index; - const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\""; - - foreach (var stream in subtitles) - { - var name = stream.DisplayTitle; - - var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index; - var isForced = stream.IsForced; - - var url = string.Format( - CultureInfo.InvariantCulture, - "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}", - state.Request.MediaSourceId, - stream.Index.ToString(CultureInfo.InvariantCulture), - 30.ToString(CultureInfo.InvariantCulture), - ClaimHelpers.GetToken(Request.HttpContext.User)); - - var line = string.Format( - CultureInfo.InvariantCulture, - Format, - name, - isDefault ? "YES" : "NO", - isForced ? "YES" : "NO", - url, - stream.Language ?? "Unknown"); - - builder.AppendLine(line); - } - } - - private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup) - { - builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=") - .Append(bitrate.ToString(CultureInfo.InvariantCulture)) - .Append(",AVERAGE-BANDWIDTH=") - .Append(bitrate.ToString(CultureInfo.InvariantCulture)); - - AppendPlaylistCodecsField(builder, state); - - AppendPlaylistResolutionField(builder, state); - - AppendPlaylistFramerateField(builder, state); - - if (!string.IsNullOrWhiteSpace(subtitleGroup)) - { - builder.Append(",SUBTITLES=\"") - .Append(subtitleGroup) - .Append('"'); - } - - builder.Append(Environment.NewLine); - builder.AppendLine(url); - } - - /// - /// Appends a CODECS field containing formatted strings of - /// the active streams output video and audio codecs. - /// - /// - /// - /// - /// StringBuilder to append the field to. - /// StreamState of the current stream. - private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state) - { - // Video - string videoCodecs = string.Empty; - int? videoCodecLevel = GetOutputVideoCodecLevel(state); - if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue) - { - videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value); - } - - // Audio - string audioCodecs = string.Empty; - if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec)) - { - audioCodecs = GetPlaylistAudioCodecs(state); - } - - StringBuilder codecs = new StringBuilder(); - - codecs.Append(videoCodecs); - - if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs)) - { - codecs.Append(','); - } - - codecs.Append(audioCodecs); - - if (codecs.Length > 1) - { - builder.Append(",CODECS=\"") - .Append(codecs) - .Append('"'); - } - } - - /// - /// Appends a RESOLUTION field containing the resolution of the output stream. - /// - /// - /// StringBuilder to append the field to. - /// StreamState of the current stream. - private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state) - { - if (state.OutputWidth.HasValue && state.OutputHeight.HasValue) - { - builder.Append(",RESOLUTION=") - .Append(state.OutputWidth.GetValueOrDefault()) - .Append('x') - .Append(state.OutputHeight.GetValueOrDefault()); - } - } - - /// - /// Appends a FRAME-RATE field containing the framerate of the output stream. - /// - /// - /// StringBuilder to append the field to. - /// StreamState of the current stream. - private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state) - { - double? framerate = null; - if (state.TargetFramerate.HasValue) - { - framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3); - } - else if (state.VideoStream?.RealFrameRate != null) - { - framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3); - } - - if (framerate.HasValue) - { - builder.Append(",FRAME-RATE=") - .Append(framerate.Value); - } - } - - private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming) - { - // Within the local network this will likely do more harm than good. - var ip = RequestHelpers.NormalizeIp(Request.HttpContext.Connection.RemoteIpAddress).ToString(); - if (_networkManager.IsInLocalNetwork(ip)) - { - return false; - } - - if (!enableAdaptiveBitrateStreaming) - { - return false; - } - - if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath)) - { - // Opening live streams is so slow it's not even worth it - return false; - } - - if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) - { - return false; - } - - if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec)) - { - return false; - } - - if (!state.IsOutputVideo) - { - return false; - } - - // Having problems in android - return false; - // return state.VideoRequest.VideoBitRate.HasValue; - } - - /// - /// Get the H.26X level of the output video stream. - /// - /// StreamState of the current stream. - /// H.26X level of the output video stream. - private int? GetOutputVideoCodecLevel(StreamState state) - { - string? levelString; - if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) - && state.VideoStream.Level.HasValue) - { - levelString = state.VideoStream?.Level.ToString(); - } - else - { - levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec); - } - - if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel)) - { - return parsedLevel; - } - - return null; - } - - /// - /// Gets a formatted string of the output audio codec, for use in the CODECS field. - /// - /// - /// - /// StreamState of the current stream. - /// Formatted audio codec string. - private string GetPlaylistAudioCodecs(StreamState state) - { - if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase)) - { - string? profile = state.GetRequestedProfiles("aac").FirstOrDefault(); - return HlsCodecStringHelpers.GetAACString(profile); - } - - if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase)) - { - return HlsCodecStringHelpers.GetMP3String(); - } - - if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase)) - { - return HlsCodecStringHelpers.GetAC3String(); - } - - if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase)) - { - return HlsCodecStringHelpers.GetEAC3String(); - } - - return string.Empty; - } - - /// - /// Gets a formatted string of the output video codec, for use in the CODECS field. - /// - /// - /// - /// StreamState of the current stream. - /// Video codec. - /// Video level. - /// Formatted video codec string. - private string GetPlaylistVideoCodecs(StreamState state, string codec, int level) - { - if (level == 0) - { - // This is 0 when there's no requested H.26X level in the device profile - // and the source is not encoded in H.26X - _logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist"); - return string.Empty; - } - - if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase)) - { - string profile = state.GetRequestedProfiles("h264").FirstOrDefault(); - return HlsCodecStringHelpers.GetH264String(profile, level); - } - - if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) - { - string profile = state.GetRequestedProfiles("h265").FirstOrDefault(); - - return HlsCodecStringHelpers.GetH265String(profile, level); - } - - return string.Empty; - } - - private int GetBitrateVariation(int bitrate) - { - // By default, vary by just 50k - var variation = 50000; - - if (bitrate >= 10000000) - { - variation = 2000000; - } - else if (bitrate >= 5000000) - { - variation = 1500000; - } - else if (bitrate >= 3000000) - { - variation = 1000000; - } - else if (bitrate >= 2000000) - { - variation = 500000; - } - else if (bitrate >= 1000000) - { - variation = 300000; - } - else if (bitrate >= 600000) - { - variation = 200000; - } - else if (bitrate >= 400000) - { - variation = 100000; - } - - return variation; - } - - private string ReplaceBitrate(string url, int oldValue, int newValue) - { - return url.Replace( - "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture), - "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture), - StringComparison.OrdinalIgnoreCase); - } - private double[] GetSegmentLengths(StreamState state) { var result = new List(); diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index 517113074..7faf479a5 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -1,30 +1,18 @@ using System; using System.Buffers; using System.ComponentModel.DataAnnotations; -using System.Globalization; using System.Linq; using System.Net.Mime; -using System.Text.Json; -using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.MediaInfoDtos; using Jellyfin.Api.Models.VideoDtos; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -42,12 +30,9 @@ namespace Jellyfin.Api.Controllers private readonly IMediaSourceManager _mediaSourceManager; private readonly IDeviceManager _deviceManager; private readonly ILibraryManager _libraryManager; - private readonly INetworkManager _networkManager; - private readonly IMediaEncoder _mediaEncoder; - private readonly IUserManager _userManager; private readonly IAuthorizationContext _authContext; private readonly ILogger _logger; - private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly MediaInfoHelper _mediaInfoHelper; /// /// Initializes a new instance of the class. @@ -55,32 +40,23 @@ namespace Jellyfin.Api.Controllers /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. + /// Instance of the . public MediaInfoController( IMediaSourceManager mediaSourceManager, IDeviceManager deviceManager, ILibraryManager libraryManager, - INetworkManager networkManager, - IMediaEncoder mediaEncoder, - IUserManager userManager, IAuthorizationContext authContext, ILogger logger, - IServerConfigurationManager serverConfigurationManager) + MediaInfoHelper mediaInfoHelper) { _mediaSourceManager = mediaSourceManager; _deviceManager = deviceManager; _libraryManager = libraryManager; - _networkManager = networkManager; - _mediaEncoder = mediaEncoder; - _userManager = userManager; _authContext = authContext; _logger = logger; - _serverConfigurationManager = serverConfigurationManager; + _mediaInfoHelper = mediaInfoHelper; } /// @@ -94,7 +70,10 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery, Required] Guid? userId) { - return await GetPlaybackInfoInternal(itemId, userId).ConfigureAwait(false); + return await _mediaInfoHelper.GetPlaybackInfo( + itemId, + userId) + .ConfigureAwait(false); } /// @@ -153,7 +132,12 @@ namespace Jellyfin.Api.Controllers } } - var info = await GetPlaybackInfoInternal(itemId, userId, mediaSourceId, liveStreamId).ConfigureAwait(false); + var info = await _mediaInfoHelper.GetPlaybackInfo( + itemId, + userId, + mediaSourceId, + liveStreamId) + .ConfigureAwait(false); if (profile != null) { @@ -162,7 +146,7 @@ namespace Jellyfin.Api.Controllers foreach (var mediaSource in info.MediaSources) { - SetDeviceSpecificData( + _mediaInfoHelper.SetDeviceSpecificData( item, mediaSource, profile, @@ -179,10 +163,11 @@ namespace Jellyfin.Api.Controllers enableDirectStream, enableTranscoding, allowVideoStreamCopy, - allowAudioStreamCopy); + allowAudioStreamCopy, + Request.HttpContext.Connection.RemoteIpAddress.ToString()); } - SortMediaSources(info, maxStreamingBitrate); + _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate); } if (autoOpenLiveStream) @@ -191,21 +176,23 @@ namespace Jellyfin.Api.Controllers if (mediaSource != null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId)) { - var openStreamResult = await OpenMediaSource(new LiveStreamRequest - { - AudioStreamIndex = audioStreamIndex, - DeviceProfile = deviceProfile?.DeviceProfile, - EnableDirectPlay = enableDirectPlay, - EnableDirectStream = enableDirectStream, - ItemId = itemId, - MaxAudioChannels = maxAudioChannels, - MaxStreamingBitrate = maxStreamingBitrate, - PlaySessionId = info.PlaySessionId, - StartTimeTicks = startTimeTicks, - SubtitleStreamIndex = subtitleStreamIndex, - UserId = userId ?? Guid.Empty, - OpenToken = mediaSource.OpenToken - }).ConfigureAwait(false); + var openStreamResult = await _mediaInfoHelper.OpenMediaSource( + Request, + new LiveStreamRequest + { + AudioStreamIndex = audioStreamIndex, + DeviceProfile = deviceProfile?.DeviceProfile, + EnableDirectPlay = enableDirectPlay, + EnableDirectStream = enableDirectStream, + ItemId = itemId, + MaxAudioChannels = maxAudioChannels, + MaxStreamingBitrate = maxStreamingBitrate, + PlaySessionId = info.PlaySessionId, + StartTimeTicks = startTimeTicks, + SubtitleStreamIndex = subtitleStreamIndex, + UserId = userId ?? Guid.Empty, + OpenToken = mediaSource.OpenToken + }).ConfigureAwait(false); info.MediaSources = new[] { openStreamResult.MediaSource }; } @@ -215,7 +202,7 @@ namespace Jellyfin.Api.Controllers { foreach (var mediaSource in info.MediaSources) { - NormalizeMediaSourceContainer(mediaSource, profile!, DlnaProfileType.Video); + _mediaInfoHelper.NormalizeMediaSourceContainer(mediaSource, profile!, DlnaProfileType.Video); } } @@ -271,7 +258,7 @@ namespace Jellyfin.Api.Controllers EnableDirectStream = enableDirectStream, DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http } }; - return await OpenMediaSource(request).ConfigureAwait(false); + return await _mediaInfoHelper.OpenMediaSource(Request, request).ConfigureAwait(false); } /// @@ -324,454 +311,5 @@ namespace Jellyfin.Api.Controllers ArrayPool.Shared.Return(buffer); } } - - private async Task GetPlaybackInfoInternal( - Guid id, - Guid? userId, - string? mediaSourceId = null, - string? liveStreamId = null) - { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; - var item = _libraryManager.GetItemById(id); - var result = new PlaybackInfoResponse(); - - MediaSourceInfo[] mediaSources; - if (string.IsNullOrWhiteSpace(liveStreamId)) - { - // TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes? - var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false); - - if (string.IsNullOrWhiteSpace(mediaSourceId)) - { - mediaSources = mediaSourcesList.ToArray(); - } - else - { - mediaSources = mediaSourcesList - .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase)) - .ToArray(); - } - } - else - { - var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false); - - mediaSources = new[] { mediaSource }; - } - - if (mediaSources.Length == 0) - { - result.MediaSources = Array.Empty(); - - result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream; - } - else - { - // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it - // Should we move this directly into MediaSourceManager? - result.MediaSources = JsonSerializer.Deserialize(JsonSerializer.SerializeToUtf8Bytes(mediaSources)); - - result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - } - - return result; - } - - private void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type) - { - mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, mediaSource.Path, profile, type); - } - - private void SetDeviceSpecificData( - BaseItem item, - MediaSourceInfo mediaSource, - DeviceProfile profile, - AuthorizationInfo auth, - long? maxBitrate, - long startTimeTicks, - string mediaSourceId, - int? audioStreamIndex, - int? subtitleStreamIndex, - int? maxAudioChannels, - string playSessionId, - Guid userId, - bool enableDirectPlay, - bool enableDirectStream, - bool enableTranscoding, - bool allowVideoStreamCopy, - bool allowAudioStreamCopy) - { - var streamBuilder = new StreamBuilder(_mediaEncoder, _logger); - - var options = new VideoOptions - { - MediaSources = new[] { mediaSource }, - Context = EncodingContext.Streaming, - DeviceId = auth.DeviceId, - ItemId = item.Id, - Profile = profile, - MaxAudioChannels = maxAudioChannels - }; - - if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase)) - { - options.MediaSourceId = mediaSourceId; - options.AudioStreamIndex = audioStreamIndex; - options.SubtitleStreamIndex = subtitleStreamIndex; - } - - var user = _userManager.GetUserById(userId); - - if (!enableDirectPlay) - { - mediaSource.SupportsDirectPlay = false; - } - - if (!enableDirectStream) - { - mediaSource.SupportsDirectStream = false; - } - - if (!enableTranscoding) - { - mediaSource.SupportsTranscoding = false; - } - - if (item is Audio) - { - _logger.LogInformation( - "User policy for {0}. EnableAudioPlaybackTranscoding: {1}", - user.Username, - user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)); - } - else - { - _logger.LogInformation( - "User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}", - user.Username, - user.HasPermission(PermissionKind.EnablePlaybackRemuxing), - user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding), - user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)); - } - - // Beginning of Playback Determination: Attempt DirectPlay first - if (mediaSource.SupportsDirectPlay) - { - if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) - { - mediaSource.SupportsDirectPlay = false; - } - else - { - var supportsDirectStream = mediaSource.SupportsDirectStream; - - // Dummy this up to fool StreamBuilder - mediaSource.SupportsDirectStream = true; - options.MaxBitrate = maxBitrate; - - if (item is Audio) - { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) - { - options.ForceDirectPlay = true; - } - } - else if (item is Video) - { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) - && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) - && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) - { - options.ForceDirectPlay = true; - } - } - - // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - ? streamBuilder.BuildAudioItem(options) - : streamBuilder.BuildVideoItem(options); - - if (streamInfo == null || !streamInfo.IsDirectStream) - { - mediaSource.SupportsDirectPlay = false; - } - - // Set this back to what it was - mediaSource.SupportsDirectStream = supportsDirectStream; - - if (streamInfo != null) - { - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - } - } - } - - if (mediaSource.SupportsDirectStream) - { - if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) - { - mediaSource.SupportsDirectStream = false; - } - else - { - options.MaxBitrate = GetMaxBitrate(maxBitrate, user); - - if (item is Audio) - { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) - { - options.ForceDirectStream = true; - } - } - else if (item is Video) - { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) - && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) - && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) - { - options.ForceDirectStream = true; - } - } - - // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - ? streamBuilder.BuildAudioItem(options) - : streamBuilder.BuildVideoItem(options); - - if (streamInfo == null || !streamInfo.IsDirectStream) - { - mediaSource.SupportsDirectStream = false; - } - - if (streamInfo != null) - { - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - } - } - } - - if (mediaSource.SupportsTranscoding) - { - options.MaxBitrate = GetMaxBitrate(maxBitrate, user); - - // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - ? streamBuilder.BuildAudioItem(options) - : streamBuilder.BuildVideoItem(options); - - if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) - { - if (streamInfo != null) - { - streamInfo.PlaySessionId = playSessionId; - streamInfo.StartPositionTicks = startTimeTicks; - mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); - mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; - mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; - mediaSource.TranscodingContainer = streamInfo.Container; - mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; - - // Do this after the above so that StartPositionTicks is set - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - } - } - else - { - if (streamInfo != null) - { - streamInfo.PlaySessionId = playSessionId; - - if (streamInfo.PlayMethod == PlayMethod.Transcode) - { - streamInfo.StartPositionTicks = startTimeTicks; - mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); - - if (!allowVideoStreamCopy) - { - mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; - } - - if (!allowAudioStreamCopy) - { - mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; - } - - mediaSource.TranscodingContainer = streamInfo.Container; - mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; - } - - if (!allowAudioStreamCopy) - { - mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; - } - - mediaSource.TranscodingContainer = streamInfo.Container; - mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; - - // Do this after the above so that StartPositionTicks is set - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - } - } - } - - foreach (var attachment in mediaSource.MediaAttachments) - { - attachment.DeliveryUrl = string.Format( - CultureInfo.InvariantCulture, - "/Videos/{0}/{1}/Attachments/{2}", - item.Id, - mediaSource.Id, - attachment.Index); - } - } - - private async Task OpenMediaSource(LiveStreamRequest request) - { - var authInfo = _authContext.GetAuthorizationInfo(Request); - - var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false); - - var profile = request.DeviceProfile; - if (profile == null) - { - var caps = _deviceManager.GetCapabilities(authInfo.DeviceId); - if (caps != null) - { - profile = caps.DeviceProfile; - } - } - - if (profile != null) - { - var item = _libraryManager.GetItemById(request.ItemId); - - SetDeviceSpecificData( - item, - result.MediaSource, - profile, - authInfo, - request.MaxStreamingBitrate, - request.StartTimeTicks ?? 0, - result.MediaSource.Id, - request.AudioStreamIndex, - request.SubtitleStreamIndex, - request.MaxAudioChannels, - request.PlaySessionId, - request.UserId, - request.EnableDirectPlay, - request.EnableDirectStream, - true, - true, - true); - } - else - { - if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl)) - { - result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId; - } - } - - // here was a check if (result.MediaSource != null) but Rider said it will never be null - NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video); - - return result; - } - - private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken) - { - var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken); - mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex; - - mediaSource.TranscodeReasons = info.TranscodeReasons; - - foreach (var profile in profiles) - { - foreach (var stream in mediaSource.MediaStreams) - { - if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index) - { - stream.DeliveryMethod = profile.DeliveryMethod; - - if (profile.DeliveryMethod == SubtitleDeliveryMethod.External) - { - stream.DeliveryUrl = profile.Url.TrimStart('-'); - stream.IsExternalUrl = profile.IsExternalUrl; - } - } - } - } - } - - private long? GetMaxBitrate(long? clientMaxBitrate, User user) - { - var maxBitrate = clientMaxBitrate; - var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0; - - if (remoteClientMaxBitrate <= 0) - { - remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit; - } - - if (remoteClientMaxBitrate > 0) - { - var isInLocalNetwork = _networkManager.IsInLocalNetwork(Request.HttpContext.Connection.RemoteIpAddress.ToString()); - - _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, Request.HttpContext.Connection.RemoteIpAddress.ToString(), isInLocalNetwork); - if (!isInLocalNetwork) - { - maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate); - } - } - - return maxBitrate; - } - - private void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate) - { - var originalList = result.MediaSources.ToList(); - - result.MediaSources = result.MediaSources.OrderBy(i => - { - // Nothing beats direct playing a file - if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File) - { - return 0; - } - - return 1; - }) - .ThenBy(i => - { - // Let's assume direct streaming a file is just as desirable as direct playing a remote url - if (i.SupportsDirectPlay || i.SupportsDirectStream) - { - return 0; - } - - return 1; - }) - .ThenBy(i => - { - return i.Protocol switch - { - MediaProtocol.File => 0, - _ => 1, - }; - }) - .ThenBy(i => - { - if (maxBitrate.HasValue && i.Bitrate.HasValue) - { - return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2; - } - - return 1; - }) - .ThenBy(originalList.IndexOf) - .ToArray(); - } } } diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 75df16aa7..2ea462488 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -2,17 +2,20 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Net.Http; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; -using Jellyfin.Api.Models.VideoDtos; +using Jellyfin.Api.Models.StreamingDtos; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.MediaInfo; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers { @@ -23,27 +26,39 @@ namespace Jellyfin.Api.Controllers public class UniversalAudioController : BaseJellyfinApiController { private readonly IAuthorizationContext _authorizationContext; - private readonly MediaInfoController _mediaInfoController; - private readonly DynamicHlsController _dynamicHlsController; - private readonly AudioController _audioController; + private readonly IDeviceManager _deviceManager; + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly MediaInfoHelper _mediaInfoHelper; + private readonly AudioHelper _audioHelper; + private readonly DynamicHlsHelper _dynamicHlsHelper; /// /// Initializes a new instance of the class. /// /// Instance of the interface. - /// Instance of the . - /// Instance of the . - /// Instance of the . + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of . + /// Instance of . + /// Instance of . public UniversalAudioController( IAuthorizationContext authorizationContext, - MediaInfoController mediaInfoController, - DynamicHlsController dynamicHlsController, - AudioController audioController) + IDeviceManager deviceManager, + ILibraryManager libraryManager, + ILogger logger, + MediaInfoHelper mediaInfoHelper, + AudioHelper audioHelper, + DynamicHlsHelper dynamicHlsHelper) { _authorizationContext = authorizationContext; - _mediaInfoController = mediaInfoController; - _dynamicHlsController = dynamicHlsController; - _audioController = audioController; + _deviceManager = deviceManager; + _libraryManager = libraryManager; + _logger = logger; + _mediaInfoHelper = mediaInfoHelper; + _audioHelper = audioHelper; + _dynamicHlsHelper = dynamicHlsHelper; } /// @@ -99,20 +114,65 @@ namespace Jellyfin.Api.Controllers var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels); _authorizationContext.GetAuthorizationInfo(Request).DeviceId = deviceId; - var playbackInfoResult = await _mediaInfoController.GetPostedPlaybackInfo( - itemId, - userId, - maxStreamingBitrate, - startTimeTicks, - null, - null, - maxAudioChannels, - mediaSourceId, - null, - new DeviceProfileDto { DeviceProfile = deviceProfile }) + var authInfo = _authorizationContext.GetAuthorizationInfo(Request); + + _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile); + + if (deviceProfile == null) + { + var caps = _deviceManager.GetCapabilities(authInfo.DeviceId); + if (caps != null) + { + deviceProfile = caps.DeviceProfile; + } + } + + var info = await _mediaInfoHelper.GetPlaybackInfo( + itemId, + userId, + mediaSourceId) .ConfigureAwait(false); - var mediaSource = playbackInfoResult.Value.MediaSources[0]; + if (deviceProfile != null) + { + // set device specific data + var item = _libraryManager.GetItemById(itemId); + + foreach (var sourceInfo in info.MediaSources) + { + _mediaInfoHelper.SetDeviceSpecificData( + item, + sourceInfo, + deviceProfile, + authInfo, + maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate, + startTimeTicks ?? 0, + mediaSourceId ?? string.Empty, + null, + null, + maxAudioChannels, + info!.PlaySessionId!, + userId ?? Guid.Empty, + true, + true, + true, + true, + true, + Request.HttpContext.Connection.RemoteIpAddress.ToString()); + } + + _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate); + } + + if (info.MediaSources != null) + { + foreach (var source in info.MediaSources) + { + _mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile!, DlnaProfileType.Video); + } + } + + var mediaSource = info.MediaSources![0]; if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http) { if (enableRedirection) @@ -127,129 +187,71 @@ namespace Jellyfin.Api.Controllers var isStatic = mediaSource.SupportsDirectStream; if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) { - var transcodingProfile = deviceProfile.TranscodingProfiles[0]; - // hls segment container can only be mpegts or fmp4 per ffmpeg documentation // TODO: remove this when we switch back to the segment muxer var supportedHlsContainers = new[] { "mpegts", "fmp4" }; - if (isHeadRequest) + var dynamicHlsRequestDto = new HlsAudioRequestDto { - _dynamicHlsController.Request.Method = HttpMethod.Head.Method; - } - - return await _dynamicHlsController.GetMasterHlsAudioPlaylist( - itemId, - ".m3u8", - isStatic, - null, - null, - null, - playbackInfoResult.Value.PlaySessionId, + Id = itemId, + Container = ".m3u8", + Static = isStatic, + PlaySessionId = info.PlaySessionId, // fallback to mpegts if device reports some weird value unsupported by hls - Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "mpegts", - null, - null, - mediaSource.Id, - deviceId, - transcodingProfile.AudioCodec, - null, - null, - null, - transcodingProfile.BreakOnNonKeyFrames, - maxAudioSampleRate, - maxAudioBitDepth, - null, - isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)), - maxAudioChannels, - null, - null, - null, - null, - null, - startTimeTicks, - null, - null, - null, - null, - SubtitleDeliveryMethod.Hls, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()), - null, - null, - EncodingContext.Static, - new Dictionary()) + SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "mpegts", + MediaSourceId = mediaSourceId, + DeviceId = deviceId, + AudioCodec = audioCodec, + EnableAutoStreamCopy = true, + AllowAudioStreamCopy = true, + AllowVideoStreamCopy = true, + BreakOnNonKeyFrames = breakOnNonKeyFrames, + AudioSampleRate = maxAudioSampleRate, + MaxAudioChannels = maxAudioChannels, + MaxAudioBitDepth = maxAudioBitDepth, + AudioChannels = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)), + StartTimeTicks = startTimeTicks, + SubtitleMethod = SubtitleDeliveryMethod.Hls, + RequireAvc = true, + DeInterlace = true, + RequireNonAnamorphic = true, + EnableMpegtsM2TsMode = true, + TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()), + Context = EncodingContext.Static, + StreamOptions = new Dictionary(), + EnableAdaptiveBitrateStreaming = true + }; + + return await _dynamicHlsHelper.GetMasterHlsPlaylist(this, TranscodingJobType.Hls, dynamicHlsRequestDto, true) .ConfigureAwait(false); } - else + + var audioStreamingDto = new StreamingRequestDto { - if (isHeadRequest) - { - _audioController.Request.Method = HttpMethod.Head.Method; - } + Id = itemId, + Container = isStatic ? null : ("." + mediaSource.TranscodingContainer), + Static = isStatic, + PlaySessionId = info.PlaySessionId, + MediaSourceId = mediaSourceId, + DeviceId = deviceId, + AudioCodec = audioCodec, + EnableAutoStreamCopy = true, + AllowAudioStreamCopy = true, + AllowVideoStreamCopy = true, + BreakOnNonKeyFrames = breakOnNonKeyFrames, + AudioSampleRate = maxAudioSampleRate, + MaxAudioChannels = maxAudioChannels, + AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)), + MaxAudioBitDepth = maxAudioBitDepth, + AudioChannels = maxAudioChannels, + CopyTimestamps = true, + StartTimeTicks = startTimeTicks, + SubtitleMethod = SubtitleDeliveryMethod.Embed, + TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()), + Context = EncodingContext.Static + }; - return await _audioController.GetAudioStream( - itemId, - isStatic ? null : ("." + mediaSource.TranscodingContainer), - isStatic, - null, - null, - null, - playbackInfoResult.Value.PlaySessionId, - null, - null, - null, - mediaSource.Id, - deviceId, - audioCodec, - null, - null, - null, - breakOnNonKeyFrames, - maxAudioSampleRate, - maxAudioBitDepth, - isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)), - null, - maxAudioChannels, - null, - null, - null, - null, - null, - startTimeTicks, - null, - null, - null, - null, - SubtitleDeliveryMethod.Embed, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()), - null, - null, - null, - null) - .ConfigureAwait(false); - } + return await _audioHelper.GetAudioStream(this, TranscodingJobType.Progressive, audioStreamingDto).ConfigureAwait(false); } private DeviceProfile GetDeviceProfile( diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs new file mode 100644 index 000000000..001028432 --- /dev/null +++ b/Jellyfin.Api/Helpers/AudioHelper.cs @@ -0,0 +1,193 @@ +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Api.Models.StreamingDtos; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Net; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; + +namespace Jellyfin.Api.Helpers +{ + /// + /// Audio helper. + /// + public class AudioHelper + { + private readonly IDlnaManager _dlnaManager; + private readonly IAuthorizationContext _authContext; + private readonly IUserManager _userManager; + private readonly ILibraryManager _libraryManager; + private readonly IMediaSourceManager _mediaSourceManager; + private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly IFileSystem _fileSystem; + private readonly ISubtitleEncoder _subtitleEncoder; + private readonly IConfiguration _configuration; + private readonly IDeviceManager _deviceManager; + private readonly TranscodingJobHelper _transcodingJobHelper; + private readonly IHttpClientFactory _httpClientFactory; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of . + /// Instance of the interface. + public AudioHelper( + IDlnaManager dlnaManager, + IAuthorizationContext authContext, + IUserManager userManager, + ILibraryManager libraryManager, + IMediaSourceManager mediaSourceManager, + IServerConfigurationManager serverConfigurationManager, + IMediaEncoder mediaEncoder, + IFileSystem fileSystem, + ISubtitleEncoder subtitleEncoder, + IConfiguration configuration, + IDeviceManager deviceManager, + TranscodingJobHelper transcodingJobHelper, + IHttpClientFactory httpClientFactory) + { + _dlnaManager = dlnaManager; + _authContext = authContext; + _userManager = userManager; + _libraryManager = libraryManager; + _mediaSourceManager = mediaSourceManager; + _serverConfigurationManager = serverConfigurationManager; + _mediaEncoder = mediaEncoder; + _fileSystem = fileSystem; + _subtitleEncoder = subtitleEncoder; + _configuration = configuration; + _deviceManager = deviceManager; + _transcodingJobHelper = transcodingJobHelper; + _httpClientFactory = httpClientFactory; + } + + /// + /// Get audio stream. + /// + /// Requesting controller. + /// Transcoding job type. + /// Streaming controller.Request dto. + /// A containing the resulting . + public async Task GetAudioStream( + BaseJellyfinApiController controller, + TranscodingJobType transcodingJobType, + StreamingRequestDto streamingRequest) + { + bool isHeadRequest = controller.Request.Method == System.Net.WebRequestMethods.Http.Head; + var cancellationTokenSource = new CancellationTokenSource(); + + using var state = await StreamingHelpers.GetStreamingState( + streamingRequest, + controller.Request, + _authContext, + _mediaSourceManager, + _userManager, + _libraryManager, + _serverConfigurationManager, + _mediaEncoder, + _fileSystem, + _subtitleEncoder, + _configuration, + _dlnaManager, + _deviceManager, + _transcodingJobHelper, + transcodingJobType, + cancellationTokenSource.Token) + .ConfigureAwait(false); + + if (streamingRequest.Static && state.DirectStreamProvider != null) + { + StreamingHelpers.AddDlnaHeaders(state, controller.Response.Headers, true, streamingRequest.StartTimeTicks, controller.Request, _dlnaManager); + + await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None) + { + AllowEndOfFile = false + }.WriteToAsync(controller.Response.Body, CancellationToken.None) + .ConfigureAwait(false); + + // TODO (moved from MediaBrowser.Api): Don't hardcode contentType + return controller.File(controller.Response.Body, MimeTypes.GetMimeType("file.ts")!); + } + + // Static remote stream + if (streamingRequest.Static && state.InputProtocol == MediaProtocol.Http) + { + StreamingHelpers.AddDlnaHeaders(state, controller.Response.Headers, true, streamingRequest.StartTimeTicks, controller.Request, _dlnaManager); + + using var httpClient = _httpClientFactory.CreateClient(); + return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, controller, httpClient).ConfigureAwait(false); + } + + if (streamingRequest.Static && state.InputProtocol != MediaProtocol.File) + { + return controller.BadRequest($"Input protocol {state.InputProtocol} cannot be streamed statically"); + } + + var outputPath = state.OutputFilePath; + var outputPathExists = System.IO.File.Exists(outputPath); + + var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive); + var isTranscodeCached = outputPathExists && transcodingJob != null; + + StreamingHelpers.AddDlnaHeaders(state, controller.Response.Headers, streamingRequest.Static || isTranscodeCached, streamingRequest.StartTimeTicks, controller.Request, _dlnaManager); + + // Static stream + if (streamingRequest.Static) + { + var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath); + + if (state.MediaSource.IsInfiniteStream) + { + await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None) + { + AllowEndOfFile = false + }.WriteToAsync(controller.Response.Body, CancellationToken.None) + .ConfigureAwait(false); + + return controller.File(controller.Response.Body, contentType); + } + + return FileStreamResponseHelpers.GetStaticFileResult( + state.MediaPath, + contentType, + isHeadRequest, + controller); + } + + // Need to start ffmpeg (because media can't be returned directly) + var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); + var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration); + var ffmpegCommandLineArguments = encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath); + return await FileStreamResponseHelpers.GetTranscodedFile( + state, + isHeadRequest, + controller, + _transcodingJobHelper, + ffmpegCommandLineArguments, + controller.Request, + transcodingJobType, + cancellationTokenSource).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs new file mode 100644 index 000000000..127d91430 --- /dev/null +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -0,0 +1,549 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Security.Claims; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Api.Models.StreamingDtos; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; + +namespace Jellyfin.Api.Helpers +{ + /// + /// Dynamic hls helper. + /// + public class DynamicHlsHelper + { + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + private readonly IDlnaManager _dlnaManager; + private readonly IAuthorizationContext _authContext; + private readonly IMediaSourceManager _mediaSourceManager; + private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly IFileSystem _fileSystem; + private readonly ISubtitleEncoder _subtitleEncoder; + private readonly IConfiguration _configuration; + private readonly IDeviceManager _deviceManager; + private readonly TranscodingJobHelper _transcodingJobHelper; + private readonly INetworkManager _networkManager; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of . + /// Instance of the interface. + /// Instance of the interface. + public DynamicHlsHelper( + ILibraryManager libraryManager, + IUserManager userManager, + IDlnaManager dlnaManager, + IAuthorizationContext authContext, + IMediaSourceManager mediaSourceManager, + IServerConfigurationManager serverConfigurationManager, + IMediaEncoder mediaEncoder, + IFileSystem fileSystem, + ISubtitleEncoder subtitleEncoder, + IConfiguration configuration, + IDeviceManager deviceManager, + TranscodingJobHelper transcodingJobHelper, + INetworkManager networkManager, + ILogger logger) + { + _libraryManager = libraryManager; + _userManager = userManager; + _dlnaManager = dlnaManager; + _authContext = authContext; + _mediaSourceManager = mediaSourceManager; + _serverConfigurationManager = serverConfigurationManager; + _mediaEncoder = mediaEncoder; + _fileSystem = fileSystem; + _subtitleEncoder = subtitleEncoder; + _configuration = configuration; + _deviceManager = deviceManager; + _transcodingJobHelper = transcodingJobHelper; + _networkManager = networkManager; + _logger = logger; + } + + /// + /// Get master hls playlist. + /// + /// Requesting controller. + /// Transcoding job type. + /// Streaming request dto. + /// Enable adaptive bitrate streaming. + /// A containing the resulting . + public async Task GetMasterHlsPlaylist( + BaseJellyfinApiController controller, + TranscodingJobType transcodingJobType, + StreamingRequestDto streamingRequest, + bool enableAdaptiveBitrateStreaming) + { + var isHeadRequest = controller.Request.Method == WebRequestMethods.Http.Head; + var cancellationTokenSource = new CancellationTokenSource(); + return await GetMasterPlaylistInternal( + controller, + streamingRequest, + isHeadRequest, + enableAdaptiveBitrateStreaming, + transcodingJobType, + cancellationTokenSource).ConfigureAwait(false); + } + + private async Task GetMasterPlaylistInternal( + BaseJellyfinApiController controller, + StreamingRequestDto streamingRequest, + bool isHeadRequest, + bool enableAdaptiveBitrateStreaming, + TranscodingJobType transcodingJobType, + CancellationTokenSource cancellationTokenSource) + { + using var state = await StreamingHelpers.GetStreamingState( + streamingRequest, + controller.Request, + _authContext, + _mediaSourceManager, + _userManager, + _libraryManager, + _serverConfigurationManager, + _mediaEncoder, + _fileSystem, + _subtitleEncoder, + _configuration, + _dlnaManager, + _deviceManager, + _transcodingJobHelper, + transcodingJobType, + cancellationTokenSource.Token) + .ConfigureAwait(false); + + controller.Response.Headers.Add(HeaderNames.Expires, "0"); + if (isHeadRequest) + { + return new FileContentResult(Array.Empty(), MimeTypes.GetMimeType("playlist.m3u8")); + } + + var totalBitrate = state.OutputAudioBitrate ?? 0 + state.OutputVideoBitrate ?? 0; + + var builder = new StringBuilder(); + + builder.AppendLine("#EXTM3U"); + + var isLiveStream = state.IsSegmentedLiveStream; + + var queryString = controller.Request.QueryString.ToString(); + + // from universal audio service + if (queryString.IndexOf("SegmentContainer", StringComparison.OrdinalIgnoreCase) == -1 && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer)) + { + queryString += "&SegmentContainer=" + state.Request.SegmentContainer; + } + + // from universal audio service + if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons) && queryString.IndexOf("TranscodeReasons=", StringComparison.OrdinalIgnoreCase) == -1) + { + queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons; + } + + // Main stream + var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8"; + + playlistUrl += queryString; + + var subtitleStreams = state.MediaSource + .MediaStreams + .Where(i => i.IsTextSubtitleStream) + .ToList(); + + var subtitleGroup = subtitleStreams.Count > 0 && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest!.EnableSubtitlesInManifest) + ? "subs" + : null; + + // If we're burning in subtitles then don't add additional subs to the manifest + if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) + { + subtitleGroup = null; + } + + if (!string.IsNullOrWhiteSpace(subtitleGroup)) + { + AddSubtitles(state, subtitleStreams, builder, controller.Request.HttpContext.User); + } + + AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); + + if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, controller.Request.HttpContext.Connection.RemoteIpAddress)) + { + var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0; + + // By default, vary by just 200k + var variation = GetBitrateVariation(totalBitrate); + + var newBitrate = totalBitrate - variation; + var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); + AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); + + variation *= 2; + newBitrate = totalBitrate - variation; + variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); + AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); + } + + return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); + } + + private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup) + { + builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=") + .Append(bitrate.ToString(CultureInfo.InvariantCulture)) + .Append(",AVERAGE-BANDWIDTH=") + .Append(bitrate.ToString(CultureInfo.InvariantCulture)); + + AppendPlaylistCodecsField(builder, state); + + AppendPlaylistResolutionField(builder, state); + + AppendPlaylistFramerateField(builder, state); + + if (!string.IsNullOrWhiteSpace(subtitleGroup)) + { + builder.Append(",SUBTITLES=\"") + .Append(subtitleGroup) + .Append('"'); + } + + builder.Append(Environment.NewLine); + builder.AppendLine(url); + } + + /// + /// Appends a CODECS field containing formatted strings of + /// the active streams output video and audio codecs. + /// + /// + /// + /// + /// StringBuilder to append the field to. + /// StreamState of the current stream. + private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state) + { + // Video + string videoCodecs = string.Empty; + int? videoCodecLevel = GetOutputVideoCodecLevel(state); + if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue) + { + videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value); + } + + // Audio + string audioCodecs = string.Empty; + if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec)) + { + audioCodecs = GetPlaylistAudioCodecs(state); + } + + StringBuilder codecs = new StringBuilder(); + + codecs.Append(videoCodecs); + + if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs)) + { + codecs.Append(','); + } + + codecs.Append(audioCodecs); + + if (codecs.Length > 1) + { + builder.Append(",CODECS=\"") + .Append(codecs) + .Append('"'); + } + } + + /// + /// Appends a RESOLUTION field containing the resolution of the output stream. + /// + /// + /// StringBuilder to append the field to. + /// StreamState of the current stream. + private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state) + { + if (state.OutputWidth.HasValue && state.OutputHeight.HasValue) + { + builder.Append(",RESOLUTION=") + .Append(state.OutputWidth.GetValueOrDefault()) + .Append('x') + .Append(state.OutputHeight.GetValueOrDefault()); + } + } + + /// + /// Appends a FRAME-RATE field containing the framerate of the output stream. + /// + /// + /// StringBuilder to append the field to. + /// StreamState of the current stream. + private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state) + { + double? framerate = null; + if (state.TargetFramerate.HasValue) + { + framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3); + } + else if (state.VideoStream?.RealFrameRate != null) + { + framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3); + } + + if (framerate.HasValue) + { + builder.Append(",FRAME-RATE=") + .Append(framerate.Value); + } + } + + private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, IPAddress ipAddress) + { + // Within the local network this will likely do more harm than good. + var ip = RequestHelpers.NormalizeIp(ipAddress).ToString(); + if (_networkManager.IsInLocalNetwork(ip)) + { + return false; + } + + if (!enableAdaptiveBitrateStreaming) + { + return false; + } + + if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath)) + { + // Opening live streams is so slow it's not even worth it + return false; + } + + if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) + { + return false; + } + + if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec)) + { + return false; + } + + if (!state.IsOutputVideo) + { + return false; + } + + // Having problems in android + return false; + // return state.VideoRequest.VideoBitRate.HasValue; + } + + private void AddSubtitles(StreamState state, IEnumerable subtitles, StringBuilder builder, ClaimsPrincipal user) + { + var selectedIndex = state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index; + const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\""; + + foreach (var stream in subtitles) + { + var name = stream.DisplayTitle; + + var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index; + var isForced = stream.IsForced; + + var url = string.Format( + CultureInfo.InvariantCulture, + "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}", + state.Request.MediaSourceId, + stream.Index.ToString(CultureInfo.InvariantCulture), + 30.ToString(CultureInfo.InvariantCulture), + ClaimHelpers.GetToken(user)); + + var line = string.Format( + CultureInfo.InvariantCulture, + Format, + name, + isDefault ? "YES" : "NO", + isForced ? "YES" : "NO", + url, + stream.Language ?? "Unknown"); + + builder.AppendLine(line); + } + } + + /// + /// Get the H.26X level of the output video stream. + /// + /// StreamState of the current stream. + /// H.26X level of the output video stream. + private int? GetOutputVideoCodecLevel(StreamState state) + { + string? levelString; + if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) + && state.VideoStream.Level.HasValue) + { + levelString = state.VideoStream?.Level.ToString(); + } + else + { + levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec); + } + + if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel)) + { + return parsedLevel; + } + + return null; + } + + /// + /// Gets a formatted string of the output audio codec, for use in the CODECS field. + /// + /// + /// + /// StreamState of the current stream. + /// Formatted audio codec string. + private string GetPlaylistAudioCodecs(StreamState state) + { + if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase)) + { + string? profile = state.GetRequestedProfiles("aac").FirstOrDefault(); + return HlsCodecStringHelpers.GetAACString(profile); + } + + if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase)) + { + return HlsCodecStringHelpers.GetMP3String(); + } + + if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase)) + { + return HlsCodecStringHelpers.GetAC3String(); + } + + if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase)) + { + return HlsCodecStringHelpers.GetEAC3String(); + } + + return string.Empty; + } + + /// + /// Gets a formatted string of the output video codec, for use in the CODECS field. + /// + /// + /// + /// StreamState of the current stream. + /// Video codec. + /// Video level. + /// Formatted video codec string. + private string GetPlaylistVideoCodecs(StreamState state, string codec, int level) + { + if (level == 0) + { + // This is 0 when there's no requested H.26X level in the device profile + // and the source is not encoded in H.26X + _logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist"); + return string.Empty; + } + + if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase)) + { + string profile = state.GetRequestedProfiles("h264").FirstOrDefault(); + return HlsCodecStringHelpers.GetH264String(profile, level); + } + + if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) + { + string profile = state.GetRequestedProfiles("h265").FirstOrDefault(); + + return HlsCodecStringHelpers.GetH265String(profile, level); + } + + return string.Empty; + } + + private int GetBitrateVariation(int bitrate) + { + // By default, vary by just 50k + var variation = 50000; + + if (bitrate >= 10000000) + { + variation = 2000000; + } + else if (bitrate >= 5000000) + { + variation = 1500000; + } + else if (bitrate >= 3000000) + { + variation = 1000000; + } + else if (bitrate >= 2000000) + { + variation = 500000; + } + else if (bitrate >= 1000000) + { + variation = 300000; + } + else if (bitrate >= 600000) + { + variation = 200000; + } + else if (bitrate >= 400000) + { + variation = 100000; + } + + return variation; + } + + private string ReplaceBitrate(string url, int oldValue, int newValue) + { + return url.Replace( + "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture), + "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture), + StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs new file mode 100644 index 000000000..e94109c91 --- /dev/null +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -0,0 +1,573 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Session; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Helpers +{ + /// + /// Media info helper. + /// + public class MediaInfoHelper + { + private readonly IUserManager _userManager; + private readonly ILibraryManager _libraryManager; + private readonly IMediaSourceManager _mediaSourceManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly ILogger _logger; + private readonly INetworkManager _networkManager; + private readonly IDeviceManager _deviceManager; + private readonly IAuthorizationContext _authContext; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public MediaInfoHelper( + IUserManager userManager, + ILibraryManager libraryManager, + IMediaSourceManager mediaSourceManager, + IMediaEncoder mediaEncoder, + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + INetworkManager networkManager, + IDeviceManager deviceManager, + IAuthorizationContext authContext) + { + _userManager = userManager; + _libraryManager = libraryManager; + _mediaSourceManager = mediaSourceManager; + _mediaEncoder = mediaEncoder; + _serverConfigurationManager = serverConfigurationManager; + _logger = logger; + _networkManager = networkManager; + _deviceManager = deviceManager; + _authContext = authContext; + } + + /// + /// Get playback info. + /// + /// Item id. + /// User Id. + /// Media source id. + /// Live stream id. + /// A containing the . + public async Task GetPlaybackInfo( + Guid id, + Guid? userId, + string? mediaSourceId = null, + string? liveStreamId = null) + { + var user = userId.HasValue && !userId.Equals(Guid.Empty) + ? _userManager.GetUserById(userId.Value) + : null; + var item = _libraryManager.GetItemById(id); + var result = new PlaybackInfoResponse(); + + MediaSourceInfo[] mediaSources; + if (string.IsNullOrWhiteSpace(liveStreamId)) + { + // TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes? + var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(mediaSourceId)) + { + mediaSources = mediaSourcesList.ToArray(); + } + else + { + mediaSources = mediaSourcesList + .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + } + } + else + { + var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false); + + mediaSources = new[] { mediaSource }; + } + + if (mediaSources.Length == 0) + { + result.MediaSources = Array.Empty(); + + result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream; + } + else + { + // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it + // Should we move this directly into MediaSourceManager? + result.MediaSources = JsonSerializer.Deserialize(JsonSerializer.SerializeToUtf8Bytes(mediaSources)); + + result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); + } + + return result; + } + + /// + /// SetDeviceSpecificData. + /// + /// Item to set data for. + /// Media source info. + /// Device profile. + /// Authorization info. + /// Max bitrate. + /// Start time ticks. + /// Media source id. + /// Audio stream index. + /// Subtitle stream index. + /// Max audio channels. + /// Play session id. + /// User id. + /// Enable direct play. + /// Enable direct stream. + /// Enable transcoding. + /// Allow video stream copy. + /// Allow audio stream copy. + /// Requesting IP address. + public void SetDeviceSpecificData( + BaseItem item, + MediaSourceInfo mediaSource, + DeviceProfile profile, + AuthorizationInfo auth, + long? maxBitrate, + long startTimeTicks, + string mediaSourceId, + int? audioStreamIndex, + int? subtitleStreamIndex, + int? maxAudioChannels, + string playSessionId, + Guid userId, + bool enableDirectPlay, + bool enableDirectStream, + bool enableTranscoding, + bool allowVideoStreamCopy, + bool allowAudioStreamCopy, + string ipAddress) + { + var streamBuilder = new StreamBuilder(_mediaEncoder, _logger); + + var options = new VideoOptions + { + MediaSources = new[] { mediaSource }, + Context = EncodingContext.Streaming, + DeviceId = auth.DeviceId, + ItemId = item.Id, + Profile = profile, + MaxAudioChannels = maxAudioChannels + }; + + if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase)) + { + options.MediaSourceId = mediaSourceId; + options.AudioStreamIndex = audioStreamIndex; + options.SubtitleStreamIndex = subtitleStreamIndex; + } + + var user = _userManager.GetUserById(userId); + + if (!enableDirectPlay) + { + mediaSource.SupportsDirectPlay = false; + } + + if (!enableDirectStream) + { + mediaSource.SupportsDirectStream = false; + } + + if (!enableTranscoding) + { + mediaSource.SupportsTranscoding = false; + } + + if (item is Audio) + { + _logger.LogInformation( + "User policy for {0}. EnableAudioPlaybackTranscoding: {1}", + user.Username, + user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)); + } + else + { + _logger.LogInformation( + "User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}", + user.Username, + user.HasPermission(PermissionKind.EnablePlaybackRemuxing), + user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding), + user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)); + } + + // Beginning of Playback Determination: Attempt DirectPlay first + if (mediaSource.SupportsDirectPlay) + { + if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) + { + mediaSource.SupportsDirectPlay = false; + } + else + { + var supportsDirectStream = mediaSource.SupportsDirectStream; + + // Dummy this up to fool StreamBuilder + mediaSource.SupportsDirectStream = true; + options.MaxBitrate = maxBitrate; + + if (item is Audio) + { + if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) + { + options.ForceDirectPlay = true; + } + } + else if (item is Video) + { + if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) + && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) + && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) + { + options.ForceDirectPlay = true; + } + } + + // The MediaSource supports direct stream, now test to see if the client supports it + var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) + ? streamBuilder.BuildAudioItem(options) + : streamBuilder.BuildVideoItem(options); + + if (streamInfo == null || !streamInfo.IsDirectStream) + { + mediaSource.SupportsDirectPlay = false; + } + + // Set this back to what it was + mediaSource.SupportsDirectStream = supportsDirectStream; + + if (streamInfo != null) + { + SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + } + } + } + + if (mediaSource.SupportsDirectStream) + { + if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) + { + mediaSource.SupportsDirectStream = false; + } + else + { + options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress); + + if (item is Audio) + { + if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) + { + options.ForceDirectStream = true; + } + } + else if (item is Video) + { + if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) + && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) + && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) + { + options.ForceDirectStream = true; + } + } + + // The MediaSource supports direct stream, now test to see if the client supports it + var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) + ? streamBuilder.BuildAudioItem(options) + : streamBuilder.BuildVideoItem(options); + + if (streamInfo == null || !streamInfo.IsDirectStream) + { + mediaSource.SupportsDirectStream = false; + } + + if (streamInfo != null) + { + SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + } + } + } + + if (mediaSource.SupportsTranscoding) + { + options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress); + + // The MediaSource supports direct stream, now test to see if the client supports it + var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) + ? streamBuilder.BuildAudioItem(options) + : streamBuilder.BuildVideoItem(options); + + if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) + { + if (streamInfo != null) + { + streamInfo.PlaySessionId = playSessionId; + streamInfo.StartPositionTicks = startTimeTicks; + mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); + mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; + mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; + mediaSource.TranscodingContainer = streamInfo.Container; + mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; + + // Do this after the above so that StartPositionTicks is set + SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + } + } + else + { + if (streamInfo != null) + { + streamInfo.PlaySessionId = playSessionId; + + if (streamInfo.PlayMethod == PlayMethod.Transcode) + { + streamInfo.StartPositionTicks = startTimeTicks; + mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); + + if (!allowVideoStreamCopy) + { + mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; + } + + if (!allowAudioStreamCopy) + { + mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; + } + + mediaSource.TranscodingContainer = streamInfo.Container; + mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; + } + + if (!allowAudioStreamCopy) + { + mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; + } + + mediaSource.TranscodingContainer = streamInfo.Container; + mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; + + // Do this after the above so that StartPositionTicks is set + SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + } + } + } + + foreach (var attachment in mediaSource.MediaAttachments) + { + attachment.DeliveryUrl = string.Format( + CultureInfo.InvariantCulture, + "/Videos/{0}/{1}/Attachments/{2}", + item.Id, + mediaSource.Id, + attachment.Index); + } + } + + /// + /// Sort media source. + /// + /// Playback info response. + /// Max bitrate. + public void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate) + { + var originalList = result.MediaSources.ToList(); + + result.MediaSources = result.MediaSources.OrderBy(i => + { + // Nothing beats direct playing a file + if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File) + { + return 0; + } + + return 1; + }) + .ThenBy(i => + { + // Let's assume direct streaming a file is just as desirable as direct playing a remote url + if (i.SupportsDirectPlay || i.SupportsDirectStream) + { + return 0; + } + + return 1; + }) + .ThenBy(i => + { + return i.Protocol switch + { + MediaProtocol.File => 0, + _ => 1, + }; + }) + .ThenBy(i => + { + if (maxBitrate.HasValue && i.Bitrate.HasValue) + { + return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2; + } + + return 1; + }) + .ThenBy(originalList.IndexOf) + .ToArray(); + } + + /// + /// Open media source. + /// + /// Http Request. + /// Live stream request. + /// A containing the . + public async Task OpenMediaSource(HttpRequest httpRequest, LiveStreamRequest request) + { + var authInfo = _authContext.GetAuthorizationInfo(httpRequest); + + var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false); + + var profile = request.DeviceProfile; + if (profile == null) + { + var caps = _deviceManager.GetCapabilities(authInfo.DeviceId); + if (caps != null) + { + profile = caps.DeviceProfile; + } + } + + if (profile != null) + { + var item = _libraryManager.GetItemById(request.ItemId); + + SetDeviceSpecificData( + item, + result.MediaSource, + profile, + authInfo, + request.MaxStreamingBitrate, + request.StartTimeTicks ?? 0, + result.MediaSource.Id, + request.AudioStreamIndex, + request.SubtitleStreamIndex, + request.MaxAudioChannels, + request.PlaySessionId, + request.UserId, + request.EnableDirectPlay, + request.EnableDirectStream, + true, + true, + true, + httpRequest.HttpContext.Connection.RemoteIpAddress.ToString()); + } + else + { + if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl)) + { + result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId; + } + } + + // here was a check if (result.MediaSource != null) but Rider said it will never be null + NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video); + + return result; + } + + /// + /// Normalize media source container. + /// + /// Media source. + /// Device profile. + /// Dlna profile type. + public void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type) + { + mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, mediaSource.Path, profile, type); + } + + private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken) + { + var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken); + mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex; + + mediaSource.TranscodeReasons = info.TranscodeReasons; + + foreach (var profile in profiles) + { + foreach (var stream in mediaSource.MediaStreams) + { + if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index) + { + stream.DeliveryMethod = profile.DeliveryMethod; + + if (profile.DeliveryMethod == SubtitleDeliveryMethod.External) + { + stream.DeliveryUrl = profile.Url.TrimStart('-'); + stream.IsExternalUrl = profile.IsExternalUrl; + } + } + } + } + } + + private long? GetMaxBitrate(long? clientMaxBitrate, User user, string ipAddress) + { + var maxBitrate = clientMaxBitrate; + var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0; + + if (remoteClientMaxBitrate <= 0) + { + remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit; + } + + if (remoteClientMaxBitrate > 0) + { + var isInLocalNetwork = _networkManager.IsInLocalNetwork(ipAddress); + + _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, ipAddress, isInLocalNetwork); + if (!isInLocalNetwork) + { + maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate); + } + } + + return maxBitrate; + } + } +} -- cgit v1.2.3 From 9e95fe8f9d402ec178024be962115abd6033fb51 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 11 Aug 2020 17:04:11 +0200 Subject: Add plugin assemblies to mvc builder --- Emby.Server.Implementations/ApplicationHost.cs | 25 +++++++++++++++++++++- .../Extensions/ApiServiceCollectionExtensions.cs | 14 ++++-------- MediaBrowser.Common/IApplicationHost.cs | 7 ++++++ 3 files changed, 35 insertions(+), 11 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 0201ed7a3..438f4384f 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -53,7 +53,6 @@ using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; using MediaBrowser.Controller; -using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Collections; @@ -98,6 +97,7 @@ using MediaBrowser.Providers.Plugins.TheTvdb; using MediaBrowser.Providers.Subtitles; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Prometheus.DotNetRuntime; @@ -1385,6 +1385,29 @@ namespace Emby.Server.Implementations _plugins = list.ToArray(); } + public IEnumerable GetApiPluginAssemblies() + { + var assemblies = new List(); + try + { + var types = _allConcreteTypes + .Where(i => typeof(ControllerBase).IsAssignableFrom(i)) + // .Select(i => ActivatorUtilities.CreateInstance(ServiceProvider, i)) + .ToArray(); + + foreach (var variable in types) + { + assemblies.Add(variable.Assembly); + } + } + catch (Exception ex) + { + // ignore + } + + return assemblies; + } + public virtual void LaunchUrl(string url) { if (!CanLaunchWebBrowser) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index d25d1ae89..13c2d6055 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -20,8 +20,6 @@ using Jellyfin.Server.Formatters; using Jellyfin.Server.Models; using MediaBrowser.Common; using MediaBrowser.Common.Json; -using MediaBrowser.Common.Plugins; -using MediaBrowser.Controller; using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; @@ -179,18 +177,14 @@ namespace Jellyfin.Server.Extensions // From JsonDefaults.PascalCase options.JsonSerializerOptions.PropertyNamingPolicy = jsonOptions.PropertyNamingPolicy; - }) - .AddControllersAsServices(); + }); - if (applicationHost.Plugins != null) + foreach (Assembly pluginAssembly in applicationHost.GetApiPluginAssemblies()) { - foreach (IPlugin plugin in applicationHost.Plugins) - { - mvcBuilder.AddApplicationPart(plugin.GetType().Assembly); - } + mvcBuilder.AddApplicationPart(pluginAssembly); } - return mvcBuilder; + return mvcBuilder.AddControllersAsServices(); } /// diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index e8d9282e4..bdbd469bd 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Reflection; using System.Threading.Tasks; using MediaBrowser.Common.Plugins; using Microsoft.Extensions.DependencyInjection; @@ -76,6 +77,12 @@ namespace MediaBrowser.Common /// The plugins. IReadOnlyList Plugins { get; } + /// + /// Gets all plugin assemblies which implement a custom rest api. + /// + /// An containing the plugin assemblies. + IEnumerable GetApiPluginAssemblies(); + /// /// Notifies the pending restart. /// -- cgit v1.2.3 From d296a1f6d0ee8cb48c2234aa9666d01035cd18b5 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 12 Aug 2020 14:00:48 +0200 Subject: Add logging, cleanup --- Emby.Server.Implementations/ApplicationHost.cs | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 438f4384f..4fb5d40c5 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1387,25 +1387,14 @@ namespace Emby.Server.Implementations public IEnumerable GetApiPluginAssemblies() { - var assemblies = new List(); - try - { - var types = _allConcreteTypes - .Where(i => typeof(ControllerBase).IsAssignableFrom(i)) - // .Select(i => ActivatorUtilities.CreateInstance(ServiceProvider, i)) - .ToArray(); + var types = _allConcreteTypes + .Where(i => typeof(ControllerBase).IsAssignableFrom(i)); - foreach (var variable in types) - { - assemblies.Add(variable.Assembly); - } - } - catch (Exception ex) + foreach (var type in types) { - // ignore + Logger.LogDebug("Found API endpoints in plugin " + type.Assembly.FullName); + yield return type.Assembly; } - - return assemblies; } public virtual void LaunchUrl(string url) -- cgit v1.2.3 From 025ee2163ffea826083fdea6a084090733ab8fff Mon Sep 17 00:00:00 2001 From: David Date: Fri, 14 Aug 2020 20:42:26 +0200 Subject: Change log message, load assembly only once --- Emby.Server.Implementations/ApplicationHost.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 4fb5d40c5..62c2e55c2 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1388,11 +1388,12 @@ namespace Emby.Server.Implementations public IEnumerable GetApiPluginAssemblies() { var types = _allConcreteTypes - .Where(i => typeof(ControllerBase).IsAssignableFrom(i)); + .Where(i => typeof(ControllerBase).IsAssignableFrom(i)) + .Distinct(); foreach (var type in types) { - Logger.LogDebug("Found API endpoints in plugin " + type.Assembly.FullName); + Logger.LogDebug("Found API endpoints in plugin {name}", type.Assembly.FullName); yield return type.Assembly; } } -- cgit v1.2.3 From a77cf53573f95c5db27470bd0701c304f7a01c9e Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sun, 16 Aug 2020 17:25:14 -0400 Subject: Allow plugins to register services. --- Emby.Server.Implementations/ApplicationHost.cs | 153 +++++++++++---------- Jellyfin.Server/CoreAppHost.cs | 25 ++-- Jellyfin.Server/Program.cs | 7 +- MediaBrowser.Common/IApplicationHost.cs | 3 +- MediaBrowser.Common/Plugins/BasePlugin.cs | 11 ++ MediaBrowser.Common/Plugins/IPlugin.cs | 13 ++ .../JellyfinApplicationFactory.cs | 7 +- 7 files changed, 126 insertions(+), 93 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 0201ed7a3..34818f12d 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -53,7 +53,6 @@ using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; using MediaBrowser.Controller; -using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Collections; @@ -173,6 +172,8 @@ namespace Emby.Server.Implementations /// protected ILogger Logger { get; } + protected IServiceCollection ServiceCollection { get; } + private IPlugin[] _plugins; /// @@ -238,9 +239,11 @@ namespace Emby.Server.Implementations ILoggerFactory loggerFactory, IStartupOptions options, IFileSystem fileSystem, - INetworkManager networkManager) + INetworkManager networkManager, + IServiceCollection serviceCollection) { _xmlSerializer = new MyXmlSerializer(); + ServiceCollection = serviceCollection; _networkManager = networkManager; networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets; @@ -464,7 +467,7 @@ namespace Emby.Server.Implementations } /// - public void Init(IServiceCollection serviceCollection) + public void Init() { HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber; HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber; @@ -493,7 +496,7 @@ namespace Emby.Server.Implementations DiscoverTypes(); - RegisterServices(serviceCollection); + RegisterServices(); } public Task ExecuteHttpHandlerAsync(HttpContext context, Func next) @@ -502,136 +505,136 @@ namespace Emby.Server.Implementations /// /// Registers services/resources with the service collection that will be available via DI. /// - protected virtual void RegisterServices(IServiceCollection serviceCollection) + protected virtual void RegisterServices() { - serviceCollection.AddSingleton(_startupOptions); + ServiceCollection.AddSingleton(_startupOptions); - serviceCollection.AddMemoryCache(); + ServiceCollection.AddMemoryCache(); - serviceCollection.AddSingleton(ConfigurationManager); - serviceCollection.AddSingleton(this); + ServiceCollection.AddSingleton(ConfigurationManager); + ServiceCollection.AddSingleton(this); - serviceCollection.AddSingleton(ApplicationPaths); + ServiceCollection.AddSingleton(ApplicationPaths); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(_fileSystemManager); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(_fileSystemManager); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(_networkManager); + ServiceCollection.AddSingleton(_networkManager); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(_xmlSerializer); + ServiceCollection.AddSingleton(_xmlSerializer); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(this); - serviceCollection.AddSingleton(ApplicationPaths); + ServiceCollection.AddSingleton(this); + ServiceCollection.AddSingleton(ApplicationPaths); - serviceCollection.AddSingleton(ServerConfigurationManager); + ServiceCollection.AddSingleton(ServerConfigurationManager); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); // TODO: Refactor to eliminate the circular dependency here so that Lazy isn't required - serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); + ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); // TODO: Refactor to eliminate the circular dependency here so that Lazy isn't required - serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); - serviceCollection.AddSingleton(); + ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); + ServiceCollection.AddSingleton(); // TODO: Refactor to eliminate the circular dependencies here so that Lazy isn't required - serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); - serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); - serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); - serviceCollection.AddSingleton(); + ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); + ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); + ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); // TODO: Refactor to eliminate the circular dependency here so that Lazy isn't required - serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); - serviceCollection.AddSingleton(); + ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); } /// @@ -831,6 +834,8 @@ namespace Emby.Server.Implementations { hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s)); } + + plugin.RegisterServices(ServiceCollection); } catch (Exception ex) { diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 04530769a..755844dd9 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -33,30 +33,33 @@ namespace Jellyfin.Server /// The to be used by the . /// The to be used by the . /// The to be used by the . + /// The to be used by the . public CoreAppHost( IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IStartupOptions options, IFileSystem fileSystem, - INetworkManager networkManager) + INetworkManager networkManager, + IServiceCollection collection) : base( applicationPaths, loggerFactory, options, fileSystem, - networkManager) + networkManager, + collection) { } /// - protected override void RegisterServices(IServiceCollection serviceCollection) + protected override void RegisterServices() { // Register an image encoder bool useSkiaEncoder = SkiaEncoder.IsNativeLibAvailable(); Type imageEncoderType = useSkiaEncoder ? typeof(SkiaEncoder) : typeof(NullImageEncoder); - serviceCollection.AddSingleton(typeof(IImageEncoder), imageEncoderType); + ServiceCollection.AddSingleton(typeof(IImageEncoder), imageEncoderType); // Log a warning if the Skia encoder could not be used if (!useSkiaEncoder) @@ -71,15 +74,15 @@ namespace Jellyfin.Server // .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"), // ServiceLifetime.Transient); - serviceCollection.AddEventServices(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddEventServices(); + ServiceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - base.RegisterServices(serviceCollection); + base.RegisterServices(); } /// diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index f6ac4e2a3..14cc5f4c2 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -154,13 +154,15 @@ namespace Jellyfin.Server ApplicationHost.LogEnvironmentInfo(_logger, appPaths); PerformStaticInitialization(); + var serviceCollection = new ServiceCollection(); var appHost = new CoreAppHost( appPaths, _loggerFactory, options, new ManagedFileSystem(_loggerFactory.CreateLogger(), appPaths), - new NetworkManager(_loggerFactory.CreateLogger())); + new NetworkManager(_loggerFactory.CreateLogger()), + serviceCollection); try { @@ -178,8 +180,7 @@ namespace Jellyfin.Server } } - ServiceCollection serviceCollection = new ServiceCollection(); - appHost.Init(serviceCollection); + appHost.Init(); var webHost = new WebHostBuilder().ConfigureWebHostBuilder(appHost, serviceCollection, options, startupConfig, appPaths).Build(); diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index e8d9282e4..7a76be722 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -116,8 +116,7 @@ namespace MediaBrowser.Common /// /// Initializes this instance. /// - /// The service collection. - void Init(IServiceCollection serviceCollection); + void Init(); /// /// Creates the instance. diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs index f10a1918f..4b2918d08 100644 --- a/MediaBrowser.Common/Plugins/BasePlugin.cs +++ b/MediaBrowser.Common/Plugins/BasePlugin.cs @@ -6,6 +6,7 @@ using System.Reflection; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; +using Microsoft.Extensions.DependencyInjection; namespace MediaBrowser.Common.Plugins { @@ -81,6 +82,16 @@ namespace MediaBrowser.Common.Plugins { } + /// + public virtual void RegisterServices(IServiceCollection serviceCollection) + { + } + + /// + public virtual void UnregisterServices(IServiceCollection serviceCollection) + { + } + /// public void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion) { diff --git a/MediaBrowser.Common/Plugins/IPlugin.cs b/MediaBrowser.Common/Plugins/IPlugin.cs index 7bd37d210..1844eb124 100644 --- a/MediaBrowser.Common/Plugins/IPlugin.cs +++ b/MediaBrowser.Common/Plugins/IPlugin.cs @@ -2,6 +2,7 @@ using System; using MediaBrowser.Model.Plugins; +using Microsoft.Extensions.DependencyInjection; namespace MediaBrowser.Common.Plugins { @@ -61,6 +62,18 @@ namespace MediaBrowser.Common.Plugins /// Called when just before the plugin is uninstalled from the server. /// void OnUninstalling(); + + /// + /// Registers the plugin's services to the service collection. + /// + /// The service collection. + void RegisterServices(IServiceCollection serviceCollection); + + /// + /// Unregisters the plugin's services from the service collection. + /// + /// The service collection. + void UnregisterServices(IServiceCollection serviceCollection); } public interface IHasPluginConfiguration diff --git a/tests/MediaBrowser.Api.Tests/JellyfinApplicationFactory.cs b/tests/MediaBrowser.Api.Tests/JellyfinApplicationFactory.cs index c39ed07de..2029f88e9 100644 --- a/tests/MediaBrowser.Api.Tests/JellyfinApplicationFactory.cs +++ b/tests/MediaBrowser.Api.Tests/JellyfinApplicationFactory.cs @@ -72,6 +72,7 @@ namespace MediaBrowser.Api.Tests var startupConfig = Program.CreateAppConfiguration(commandLineOpts, appPaths); ILoggerFactory loggerFactory = new SerilogLoggerFactory(); + var serviceCollection = new ServiceCollection(); _disposableComponents.Add(loggerFactory); // Create the app host and initialize it @@ -80,10 +81,10 @@ namespace MediaBrowser.Api.Tests loggerFactory, commandLineOpts, new ManagedFileSystem(loggerFactory.CreateLogger(), appPaths), - new NetworkManager(loggerFactory.CreateLogger())); + new NetworkManager(loggerFactory.CreateLogger()), + serviceCollection); _disposableComponents.Add(appHost); - var serviceCollection = new ServiceCollection(); - appHost.Init(serviceCollection); + appHost.Init(); // Configure the web host builder Program.ConfigureWebHostBuilder(builder, appHost, serviceCollection, commandLineOpts, startupConfig, appPaths); -- cgit v1.2.3 From df0d197dc8509b38c6b4e5bd9ec442810bc0c647 Mon Sep 17 00:00:00 2001 From: Matt Montgomery <33811686+ConfusedPolarBear@users.noreply.github.com> Date: Wed, 26 Aug 2020 15:24:24 -0500 Subject: Apply suggestions from code review --- Emby.Server.Implementations/ApplicationHost.cs | 2 +- Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 2f578b3e3..257e78b9b 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -37,6 +37,7 @@ using Emby.Server.Implementations.LiveTv; using Emby.Server.Implementations.Localization; using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Playlists; +using Emby.Server.Implementations.QuickConnect; using Emby.Server.Implementations.ScheduledTasks; using Emby.Server.Implementations.Security; using Emby.Server.Implementations.Serialization; @@ -102,7 +103,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Prometheus.DotNetRuntime; using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; -using Emby.Server.Implementations.QuickConnect; namespace Emby.Server.Implementations { diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs index 52e934229..140a67541 100644 --- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs +++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs @@ -204,7 +204,7 @@ namespace Emby.Server.Implementations.QuickConnect UserId = user }); - var tokens = raw.Items.Where(x => x.AppName.StartsWith(TokenName, StringComparison.CurrentCulture)); + var tokens = raw.Items.Where(x => x.AppName.StartsWith(TokenName, StringComparison.Ordinal)); var removed = 0; foreach (var token in tokens) -- cgit v1.2.3 From 7f79f2ee0e1fe3b5f0edb22677df9f9235d883fa Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Mon, 31 Aug 2020 17:53:55 +0200 Subject: Use .Distinct on assembly --- Emby.Server.Implementations/ApplicationHost.cs | 10 ++++++---- Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 6 +++--- Jellyfin.Server/Startup.cs | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 62c2e55c2..d2f016f7d 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1387,15 +1387,17 @@ namespace Emby.Server.Implementations public IEnumerable GetApiPluginAssemblies() { - var types = _allConcreteTypes + var assemblies = _allConcreteTypes .Where(i => typeof(ControllerBase).IsAssignableFrom(i)) + .Select(i => i.Assembly) .Distinct(); - foreach (var type in types) + foreach (var assembly in assemblies) { - Logger.LogDebug("Found API endpoints in plugin {name}", type.Assembly.FullName); - yield return type.Assembly; + Logger.LogDebug("Found API endpoints in plugin {name}", assembly.FullName); } + + return assemblies; } public virtual void LaunchUrl(string url) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 13c2d6055..1c872efe0 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -136,9 +136,9 @@ namespace Jellyfin.Server.Extensions /// /// The service collection. /// The base url for the API. - /// The application host. + /// An IEnumberable containing all plugin assemblies with API controllers. /// The MVC builder. - public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, string baseUrl, IApplicationHost applicationHost) + public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, string baseUrl, IEnumerable pluginAssemblies) { IMvcBuilder mvcBuilder = serviceCollection .AddCors(options => @@ -179,7 +179,7 @@ namespace Jellyfin.Server.Extensions options.JsonSerializerOptions.PropertyNamingPolicy = jsonOptions.PropertyNamingPolicy; }); - foreach (Assembly pluginAssembly in applicationHost.GetApiPluginAssemblies()) + foreach (Assembly pluginAssembly in pluginAssemblies) { mvcBuilder.AddApplicationPart(pluginAssembly); } diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 7c54334d1..ef255fb60 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -41,7 +41,7 @@ namespace Jellyfin.Server { services.AddResponseCompression(); services.AddHttpContextAccessor(); - services.AddJellyfinApi(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/'), _applicationHost); + services.AddJellyfinApi(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/'), _applicationHost.GetApiPluginAssemblies()); services.AddJellyfinApiSwagger(); -- cgit v1.2.3 From b37cc7bfaf51bd205d2251daf984b35bfaad98b2 Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Mon, 31 Aug 2020 18:03:13 +0200 Subject: Don't evaluate twice --- Emby.Server.Implementations/ApplicationHost.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index d2f016f7d..09084249e 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1395,9 +1395,8 @@ namespace Emby.Server.Implementations foreach (var assembly in assemblies) { Logger.LogDebug("Found API endpoints in plugin {name}", assembly.FullName); + yield return assembly; } - - return assemblies; } public virtual void LaunchUrl(string url) -- cgit v1.2.3 From 6d19adbecfb7ef255de7ec0ece28dd96dc2c6772 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 31 Aug 2020 11:36:29 -0600 Subject: migrate to IHttpClientFactory in ApplicationHost --- Emby.Server.Implementations/ApplicationHost.cs | 33 ++++++++++---------------- 1 file changed, 13 insertions(+), 20 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index a2d6e2c9e..9ae010d7c 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -49,6 +49,7 @@ using Jellyfin.Api.Helpers; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; +using MediaBrowser.Common.Json; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; @@ -123,7 +124,7 @@ namespace Emby.Server.Implementations private IMediaEncoder _mediaEncoder; private ISessionManager _sessionManager; private IHttpServer _httpServer; - private IHttpClient _httpClient; + private IHttpClientFactory _httpClientFactory; /// /// Gets a value indicating whether this instance can self restart. @@ -653,7 +654,7 @@ namespace Emby.Server.Implementations _mediaEncoder = Resolve(); _sessionManager = Resolve(); _httpServer = Resolve(); - _httpClient = Resolve(); + _httpClientFactory = Resolve(); ((AuthenticationRepository)Resolve()).Initialize(); @@ -1298,25 +1299,17 @@ namespace Emby.Server.Implementations try { - using (var response = await _httpClient.SendAsync( - new HttpRequestOptions - { - Url = apiUrl, - LogErrorResponseBody = false, - BufferContent = false, - CancellationToken = cancellationToken - }, HttpMethod.Post).ConfigureAwait(false)) - { - using (var reader = new StreamReader(response.Content)) - { - var result = await reader.ReadToEndAsync().ConfigureAwait(false); - var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase); + using var request = new HttpRequestMessage(HttpMethod.Post, apiUrl); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .SendAsync(request, cancellationToken).ConfigureAwait(false); - _validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid); - Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, valid); - return valid; - } - } + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + var result = await System.Text.Json.JsonSerializer.DeserializeAsync(stream, JsonDefaults.GetOptions(), cancellationToken).ConfigureAwait(false); + var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase); + + _validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid); + Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, valid); + return valid; } catch (OperationCanceledException) { -- cgit v1.2.3 From f498e1ee59af1413cd4f041227b0296f9ec02a21 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 31 Aug 2020 12:10:49 -0600 Subject: remove IHttpClient 🎉 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Emby.Server.Implementations/ApplicationHost.cs | 2 - .../HttpClientManager/HttpClientManager.cs | 335 --------------------- MediaBrowser.Common/Net/IHttpClient.cs | 53 ---- 3 files changed, 390 deletions(-) delete mode 100644 Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs delete mode 100644 MediaBrowser.Common/Net/IHttpClient.cs (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 9ae010d7c..fbf4aef8b 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -523,8 +523,6 @@ namespace Emby.Server.Implementations ServiceCollection.AddSingleton(_fileSystemManager); ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(_networkManager); ServiceCollection.AddSingleton(); diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs deleted file mode 100644 index 25adc5812..000000000 --- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs +++ /dev/null @@ -1,335 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using MediaBrowser.Common; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; - -namespace Emby.Server.Implementations.HttpClientManager -{ - /// - /// Class HttpClientManager. - /// - public class HttpClientManager : IHttpClient - { - private readonly ILogger _logger; - private readonly IApplicationPaths _appPaths; - private readonly IFileSystem _fileSystem; - private readonly IApplicationHost _appHost; - - /// - /// Holds a dictionary of http clients by host. Use GetHttpClient(host) to retrieve or create a client for web requests. - /// DON'T dispose it after use. - /// - /// The HTTP clients. - private readonly ConcurrentDictionary _httpClients = new ConcurrentDictionary(); - - /// - /// Initializes a new instance of the class. - /// - public HttpClientManager( - IApplicationPaths appPaths, - ILogger logger, - IFileSystem fileSystem, - IApplicationHost appHost) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _fileSystem = fileSystem; - _appPaths = appPaths ?? throw new ArgumentNullException(nameof(appPaths)); - _appHost = appHost; - } - - /// - /// Gets the correct http client for the given url. - /// - /// The url. - /// HttpClient. - private HttpClient GetHttpClient(string url) - { - var key = GetHostFromUrl(url); - - if (!_httpClients.TryGetValue(key, out var client)) - { - client = new HttpClient() - { - BaseAddress = new Uri(url) - }; - - _httpClients.TryAdd(key, client); - } - - return client; - } - - private HttpRequestMessage GetRequestMessage(HttpRequestOptions options, HttpMethod method) - { - string url = options.Url; - var uriAddress = new Uri(url); - string userInfo = uriAddress.UserInfo; - if (!string.IsNullOrWhiteSpace(userInfo)) - { - _logger.LogWarning("Found userInfo in url: {0} ... url: {1}", userInfo, url); - url = url.Replace(userInfo + '@', string.Empty, StringComparison.Ordinal); - } - - var request = new HttpRequestMessage(method, url); - - foreach (var header in options.RequestHeaders) - { - request.Headers.TryAddWithoutValidation(header.Key, header.Value); - } - - if (options.EnableDefaultUserAgent - && !request.Headers.TryGetValues(HeaderNames.UserAgent, out _)) - { - request.Headers.Add(HeaderNames.UserAgent, _appHost.ApplicationUserAgent); - } - - switch (options.DecompressionMethod) - { - case CompressionMethods.Deflate | CompressionMethods.Gzip: - request.Headers.Add(HeaderNames.AcceptEncoding, new[] { "gzip", "deflate" }); - break; - case CompressionMethods.Deflate: - request.Headers.Add(HeaderNames.AcceptEncoding, "deflate"); - break; - case CompressionMethods.Gzip: - request.Headers.Add(HeaderNames.AcceptEncoding, "gzip"); - break; - default: - break; - } - - if (options.EnableKeepAlive) - { - request.Headers.Add(HeaderNames.Connection, "Keep-Alive"); - } - - // request.Headers.Add(HeaderNames.CacheControl, "no-cache"); - - /* - if (!string.IsNullOrWhiteSpace(userInfo)) - { - var parts = userInfo.Split(':'); - if (parts.Length == 2) - { - request.Headers.Add(HeaderNames., GetCredential(url, parts[0], parts[1]); - } - } - */ - - return request; - } - - /// - /// Gets the response internal. - /// - /// The options. - /// Task{HttpResponseInfo}. - public Task GetResponse(HttpRequestOptions options) - => SendAsync(options, HttpMethod.Get); - - /// - /// Performs a GET request and returns the resulting stream. - /// - /// The options. - /// Task{Stream}. - public async Task Get(HttpRequestOptions options) - { - var response = await GetResponse(options).ConfigureAwait(false); - return response.Content; - } - - /// - /// send as an asynchronous operation. - /// - /// The options. - /// The HTTP method. - /// Task{HttpResponseInfo}. - public Task SendAsync(HttpRequestOptions options, string httpMethod) - => SendAsync(options, new HttpMethod(httpMethod)); - - /// - /// send as an asynchronous operation. - /// - /// The options. - /// The HTTP method. - /// Task{HttpResponseInfo}. - public async Task SendAsync(HttpRequestOptions options, HttpMethod httpMethod) - { - if (options.CacheMode == CacheMode.None) - { - return await SendAsyncInternal(options, httpMethod).ConfigureAwait(false); - } - - var url = options.Url; - var urlHash = url.ToUpperInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture); - - var responseCachePath = Path.Combine(_appPaths.CachePath, "httpclient", urlHash); - - var response = GetCachedResponse(responseCachePath, options.CacheLength, url); - if (response != null) - { - return response; - } - - response = await SendAsyncInternal(options, httpMethod).ConfigureAwait(false); - - if (response.StatusCode == HttpStatusCode.OK) - { - await CacheResponse(response, responseCachePath).ConfigureAwait(false); - } - - return response; - } - - private HttpResponseInfo GetCachedResponse(string responseCachePath, TimeSpan cacheLength, string url) - { - if (File.Exists(responseCachePath) - && _fileSystem.GetLastWriteTimeUtc(responseCachePath).Add(cacheLength) > DateTime.UtcNow) - { - var stream = new FileStream(responseCachePath, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true); - - return new HttpResponseInfo - { - ResponseUrl = url, - Content = stream, - StatusCode = HttpStatusCode.OK, - ContentLength = stream.Length - }; - } - - return null; - } - - private async Task CacheResponse(HttpResponseInfo response, string responseCachePath) - { - Directory.CreateDirectory(Path.GetDirectoryName(responseCachePath)); - - using (var fileStream = new FileStream( - responseCachePath, - FileMode.Create, - FileAccess.Write, - FileShare.None, - IODefaults.FileStreamBufferSize, - true)) - { - await response.Content.CopyToAsync(fileStream).ConfigureAwait(false); - - response.Content.Position = 0; - } - } - - private async Task SendAsyncInternal(HttpRequestOptions options, HttpMethod httpMethod) - { - ValidateParams(options); - - options.CancellationToken.ThrowIfCancellationRequested(); - - var client = GetHttpClient(options.Url); - - var httpWebRequest = GetRequestMessage(options, httpMethod); - - if (!string.IsNullOrEmpty(options.RequestContent) - || httpMethod == HttpMethod.Post) - { - if (options.RequestContent != null) - { - httpWebRequest.Content = new StringContent( - options.RequestContent, - null, - options.RequestContentType); - } - else - { - httpWebRequest.Content = new ByteArrayContent(Array.Empty()); - } - } - - options.CancellationToken.ThrowIfCancellationRequested(); - - var response = await client.SendAsync( - httpWebRequest, - options.BufferContent || options.CacheMode == CacheMode.Unconditional ? HttpCompletionOption.ResponseContentRead : HttpCompletionOption.ResponseHeadersRead, - options.CancellationToken).ConfigureAwait(false); - - await EnsureSuccessStatusCode(response, options).ConfigureAwait(false); - - options.CancellationToken.ThrowIfCancellationRequested(); - - var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - return new HttpResponseInfo(response.Headers, response.Content.Headers) - { - Content = stream, - StatusCode = response.StatusCode, - ContentType = response.Content.Headers.ContentType?.MediaType, - ContentLength = response.Content.Headers.ContentLength, - ResponseUrl = response.Content.Headers.ContentLocation?.ToString() - }; - } - - /// - public Task Post(HttpRequestOptions options) - => SendAsync(options, HttpMethod.Post); - - private void ValidateParams(HttpRequestOptions options) - { - if (string.IsNullOrEmpty(options.Url)) - { - throw new ArgumentNullException(nameof(options)); - } - } - - /// - /// Gets the host from URL. - /// - /// The URL. - /// System.String. - private static string GetHostFromUrl(string url) - { - var index = url.IndexOf("://", StringComparison.OrdinalIgnoreCase); - - if (index != -1) - { - url = url.Substring(index + 3); - var host = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); - - if (!string.IsNullOrWhiteSpace(host)) - { - return host; - } - } - - return url; - } - - private async Task EnsureSuccessStatusCode(HttpResponseMessage response, HttpRequestOptions options) - { - if (response.IsSuccessStatusCode) - { - return; - } - - if (options.LogErrorResponseBody) - { - string msg = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - _logger.LogError("HTTP request failed with message: {Message}", msg); - } - - throw new HttpException(response.ReasonPhrase) - { - StatusCode = response.StatusCode - }; - } - } -} diff --git a/MediaBrowser.Common/Net/IHttpClient.cs b/MediaBrowser.Common/Net/IHttpClient.cs deleted file mode 100644 index 534e22edd..000000000 --- a/MediaBrowser.Common/Net/IHttpClient.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.IO; -using System.Net.Http; -using System.Threading.Tasks; - -namespace MediaBrowser.Common.Net -{ - /// - /// Interface IHttpClient. - /// - public interface IHttpClient - { - /// - /// Gets the response. - /// - /// The options. - /// Task{HttpResponseInfo}. - Task GetResponse(HttpRequestOptions options); - - /// - /// Gets the specified options. - /// - /// The options. - /// Task{Stream}. - Task Get(HttpRequestOptions options); - - /// - /// Warning: Deprecated function, - /// use 'Task{HttpResponseInfo} SendAsync(HttpRequestOptions options, HttpMethod httpMethod);' instead - /// Sends the asynchronous. - /// - /// The options. - /// The HTTP method. - /// Task{HttpResponseInfo}. - [Obsolete("Use 'Task{HttpResponseInfo} SendAsync(HttpRequestOptions options, HttpMethod httpMethod);' instead")] - Task SendAsync(HttpRequestOptions options, string httpMethod); - - /// - /// Sends the asynchronous. - /// - /// The options. - /// The HTTP method. - /// Task{HttpResponseInfo}. - Task SendAsync(HttpRequestOptions options, HttpMethod httpMethod); - - /// - /// Posts the specified options. - /// - /// The options. - /// Task{HttpResponseInfo}. - Task Post(HttpRequestOptions options); - } -} -- cgit v1.2.3 From e653eef44ff5dd7e568816d43b9e0d50b2293387 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Mon, 31 Aug 2020 22:20:19 +0200 Subject: Fix some warnings --- Emby.Dlna/ContentDirectory/ControlHandler.cs | 2 +- Emby.Dlna/Didl/DidlBuilder.cs | 4 +- .../AppBase/BaseConfigurationManager.cs | 2 +- Emby.Server.Implementations/ApplicationHost.cs | 12 +++-- .../Channels/ChannelManager.cs | 2 +- Emby.Server.Implementations/Dto/DtoService.cs | 2 +- Emby.Server.Implementations/IO/FileRefresher.cs | 7 +-- Emby.Server.Implementations/IO/LibraryMonitor.cs | 2 +- .../IO/ManagedFileSystem.cs | 32 ------------ Emby.Server.Implementations/IO/StreamHelper.cs | 32 +----------- .../LiveTv/EmbyTV/DirectRecorder.cs | 21 +++++--- .../LiveTv/EmbyTV/EmbyTV.cs | 22 +------- .../LiveTv/EmbyTV/EncodedRecorder.cs | 43 ++++------------ .../LiveTv/Listings/SchedulesDirect.cs | 23 +++++---- .../ScheduledTasks/Tasks/DeleteCacheFileTask.cs | 59 ++++++++++------------ .../ScheduledTasks/Tasks/PluginUpdateTask.cs | 42 +++++++-------- .../ScheduledTasks/Triggers/DailyTrigger.cs | 12 ++--- .../ScheduledTasks/Triggers/IntervalTrigger.cs | 14 ++--- .../ScheduledTasks/Triggers/StartupTrigger.cs | 15 +++--- .../ScheduledTasks/Triggers/WeeklyTrigger.cs | 17 +++---- MediaBrowser.Controller/LiveTv/ChannelInfo.cs | 1 + MediaBrowser.Model/IO/FileSystemMetadata.cs | 2 +- MediaBrowser.Model/IO/IFileSystem.cs | 4 +- MediaBrowser.Model/IO/IShortcutHandler.cs | 1 - MediaBrowser.Model/IO/IStreamHelper.cs | 2 - MediaBrowser.Model/IO/IZipClient.cs | 1 + .../Plugins/Tmdb/People/TmdbPersonImageProvider.cs | 8 +-- .../Plugins/Tmdb/People/TmdbPersonProvider.cs | 18 +++++-- 28 files changed, 156 insertions(+), 246 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index be1ed7872..4b108b89e 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -1363,7 +1363,7 @@ namespace Emby.Dlna.ContentDirectory }; } - Logger.LogError("Error parsing item Id: {id}. Returning user root folder.", id); + Logger.LogError("Error parsing item Id: {Id}. Returning user root folder.", id); return new ServerItem(_libraryManager.GetUserRootFolder()); } diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index bd09a8051..5b8a89d8f 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -948,7 +948,7 @@ namespace Emby.Dlna.Didl } catch (XmlException ex) { - _logger.LogError(ex, "Error adding xml value: {value}", name); + _logger.LogError(ex, "Error adding xml value: {Value}", name); } } @@ -960,7 +960,7 @@ namespace Emby.Dlna.Didl } catch (XmlException ex) { - _logger.LogError(ex, "Error adding xml value: {value}", value); + _logger.LogError(ex, "Error adding xml value: {Value}", value); } } diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index d4a8268b9..4ab0a2a3f 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -308,7 +308,7 @@ namespace Emby.Server.Implementations.AppBase } catch (Exception ex) { - Logger.LogError(ex, "Error loading configuration file: {path}", path); + Logger.LogError(ex, "Error loading configuration file: {Path}", path); return Activator.CreateInstance(configurationType); } diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index a2d6e2c9e..8b73e3610 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -275,6 +275,10 @@ namespace Emby.Server.Implementations Password = ServerConfigurationManager.Configuration.CertificatePassword }; Certificate = GetCertificate(CertificateInfo); + + ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version; + ApplicationVersionString = ApplicationVersion.ToString(3); + ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString; } public string ExpandVirtualPath(string path) @@ -304,16 +308,16 @@ namespace Emby.Server.Implementations } /// - public Version ApplicationVersion { get; } = typeof(ApplicationHost).Assembly.GetName().Version; + public Version ApplicationVersion { get; } /// - public string ApplicationVersionString { get; } = typeof(ApplicationHost).Assembly.GetName().Version.ToString(3); + public string ApplicationVersionString { get; } /// /// Gets the current application user agent. /// /// The application user agent. - public string ApplicationUserAgent => Name.Replace(' ', '-') + "/" + ApplicationVersionString; + public string ApplicationUserAgent { get; } /// /// Gets the email address for use within a comment section of a user agent field. @@ -1403,7 +1407,7 @@ namespace Emby.Server.Implementations foreach (var assembly in assemblies) { - Logger.LogDebug("Found API endpoints in plugin {name}", assembly.FullName); + Logger.LogDebug("Found API endpoints in plugin {Name}", assembly.FullName); yield return assembly; } } diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 26fc1bee4..fb1bb65a0 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -890,7 +890,7 @@ namespace Emby.Server.Implementations.Channels } catch (Exception ex) { - _logger.LogError(ex, "Error writing to channel cache file: {path}", path); + _logger.LogError(ex, "Error writing to channel cache file: {Path}", path); } } diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index f2c7118fe..57c1398e9 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -197,7 +197,7 @@ namespace Emby.Server.Implementations.Dto catch (Exception ex) { // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions - _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {itemName}", item.Name); + _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {ItemName}", item.Name); } } diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index fe74f1de7..7435e9d0b 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -149,7 +149,7 @@ namespace Emby.Server.Implementations.IO continue; } - _logger.LogInformation("{name} ({path}) will be refreshed.", item.Name, item.Path); + _logger.LogInformation("{Name} ({Path}) will be refreshed.", item.Name, item.Path); try { @@ -160,11 +160,11 @@ namespace Emby.Server.Implementations.IO // For now swallow and log. // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable) // Should we remove it from it's parent? - _logger.LogError(ex, "Error refreshing {name}", item.Name); + _logger.LogError(ex, "Error refreshing {Name}", item.Name); } catch (Exception ex) { - _logger.LogError(ex, "Error refreshing {name}", item.Name); + _logger.LogError(ex, "Error refreshing {Name}", item.Name); } } } @@ -214,6 +214,7 @@ namespace Emby.Server.Implementations.IO } } + /// public void Dispose() { _disposed = true; diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index 9290dfcd0..3353fae9d 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -88,7 +88,7 @@ namespace Emby.Server.Implementations.IO } catch (Exception ex) { - _logger.LogError(ex, "Error in ReportFileSystemChanged for {path}", path); + _logger.LogError(ex, "Error in ReportFileSystemChanged for {Path}", path); } } } diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index ab6483bf9..3cb025111 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -398,30 +398,6 @@ namespace Emby.Server.Implementations.IO } } - public virtual void SetReadOnly(string path, bool isReadOnly) - { - if (OperatingSystem.Id != OperatingSystemId.Windows) - { - return; - } - - var info = GetExtendedFileSystemInfo(path); - - if (info.Exists && info.IsReadOnly != isReadOnly) - { - if (isReadOnly) - { - File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.ReadOnly); - } - else - { - var attributes = File.GetAttributes(path); - attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly); - File.SetAttributes(path, attributes); - } - } - } - public virtual void SetAttributes(string path, bool isHidden, bool isReadOnly) { if (OperatingSystem.Id != OperatingSystemId.Windows) @@ -707,14 +683,6 @@ namespace Emby.Server.Implementations.IO return Directory.EnumerateFileSystemEntries(path, "*", searchOption); } - public virtual void SetExecutable(string path) - { - if (OperatingSystem.Id == OperatingSystemId.Darwin) - { - RunProcess("chmod", "+x \"" + path + "\"", Path.GetDirectoryName(path)); - } - } - private static void RunProcess(string path, string args, string workingDirectory) { using (var process = Process.Start(new ProcessStartInfo diff --git a/Emby.Server.Implementations/IO/StreamHelper.cs b/Emby.Server.Implementations/IO/StreamHelper.cs index 40b397edc..c16ebd61b 100644 --- a/Emby.Server.Implementations/IO/StreamHelper.cs +++ b/Emby.Server.Implementations/IO/StreamHelper.cs @@ -11,8 +11,6 @@ namespace Emby.Server.Implementations.IO { public class StreamHelper : IStreamHelper { - private const int StreamCopyToBufferSize = 81920; - public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken) { byte[] buffer = ArrayPool.Shared.Rent(bufferSize); @@ -83,37 +81,9 @@ namespace Emby.Server.Implementations.IO } } - public async Task CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken) - { - byte[] buffer = ArrayPool.Shared.Rent(StreamCopyToBufferSize); - try - { - int totalBytesRead = 0; - - int bytesRead; - while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) - { - var bytesToWrite = bytesRead; - - if (bytesToWrite > 0) - { - await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); - - totalBytesRead += bytesRead; - } - } - - return totalBytesRead; - } - finally - { - ArrayPool.Shared.Return(buffer); - } - } - public async Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) { - byte[] buffer = ArrayPool.Shared.Rent(StreamCopyToBufferSize); + byte[] buffer = ArrayPool.Shared.Rent(IODefaults.CopyToBufferSize); try { int bytesRead; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs index 2e13a3bb3..0edd98031 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -52,10 +52,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _logger.LogInformation("Copying recording stream to file {0}", targetFile); // The media source is infinite so we need to handle stopping ourselves - var durationToken = new CancellationTokenSource(duration); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; + using var durationToken = new CancellationTokenSource(duration); + using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token); - await directStreamProvider.CopyToAsync(output, cancellationToken).ConfigureAwait(false); + await directStreamProvider.CopyToAsync(output, cancellationTokenSource.Token).ConfigureAwait(false); } _logger.LogInformation("Recording completed to file {0}", targetFile); @@ -72,7 +72,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV UserAgent = "Emby/3.0", // Shouldn't matter but may cause issues - DecompressionMethod = CompressionMethods.None + DecompressionMethod = CompressionMethods.None, + CancellationToken = cancellationToken }; using (var response = await _httpClient.SendAsync(httpRequestOptions, HttpMethod.Get).ConfigureAwait(false)) @@ -88,10 +89,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _logger.LogInformation("Copying recording stream to file {0}", targetFile); // The media source if infinite so we need to handle stopping ourselves - var durationToken = new CancellationTokenSource(duration); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; - - await _streamHelper.CopyUntilCancelled(response.Content, output, 81920, cancellationToken).ConfigureAwait(false); + using var durationToken = new CancellationTokenSource(duration); + using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token); + + await _streamHelper.CopyUntilCancelled( + response.Content, + output, + IODefaults.CopyToBufferSize, + cancellationTokenSource.Token).ConfigureAwait(false); } } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 09c52d95b..5cf09b8e5 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -604,11 +604,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return Task.CompletedTask; } - public Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken) { throw new NotImplementedException(); @@ -808,11 +803,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return null; } - public IEnumerable GetAllActiveRecordings() - { - return _activeRecordings.Values.Where(i => i.Timer.Status == RecordingStatus.InProgress && !i.CancellationTokenSource.IsCancellationRequested); - } - public ActiveRecordingInfo GetActiveRecordingInfo(string path) { if (string.IsNullOrWhiteSpace(path)) @@ -1015,16 +1005,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV throw new Exception("Tuner not found."); } - private MediaSourceInfo CloneMediaSource(MediaSourceInfo mediaSource, bool enableStreamSharing) - { - var json = _jsonSerializer.SerializeToString(mediaSource); - mediaSource = _jsonSerializer.DeserializeFromString(json); - - mediaSource.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture) + "_" + mediaSource.Id; - - return mediaSource; - } - public async Task> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(channelId)) @@ -1654,7 +1634,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http)) { - return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _config); + return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer); } return new DirectRecorder(_logger, _httpClient, _streamHelper); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 612dc5238..3e5457dbd 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -8,12 +8,9 @@ using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; @@ -26,26 +23,24 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private readonly ILogger _logger; private readonly IMediaEncoder _mediaEncoder; private readonly IServerApplicationPaths _appPaths; + private readonly IJsonSerializer _json; + private readonly TaskCompletionSource _taskCompletionSource = new TaskCompletionSource(); + private bool _hasExited; private Stream _logFileStream; private string _targetPath; private Process _process; - private readonly IJsonSerializer _json; - private readonly TaskCompletionSource _taskCompletionSource = new TaskCompletionSource(); - private readonly IServerConfigurationManager _config; public EncodedRecorder( ILogger logger, IMediaEncoder mediaEncoder, IServerApplicationPaths appPaths, - IJsonSerializer json, - IServerConfigurationManager config) + IJsonSerializer json) { _logger = logger; _mediaEncoder = mediaEncoder; _appPaths = appPaths; _json = json; - _config = config; } private static bool CopySubtitles => false; @@ -58,19 +53,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public async Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { // The media source is infinite so we need to handle stopping ourselves - var durationToken = new CancellationTokenSource(duration); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; + using var durationToken = new CancellationTokenSource(duration); + using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token); - await RecordFromFile(mediaSource, mediaSource.Path, targetFile, duration, onStarted, cancellationToken).ConfigureAwait(false); + await RecordFromFile(mediaSource, mediaSource.Path, targetFile, duration, onStarted, cancellationTokenSource.Token).ConfigureAwait(false); _logger.LogInformation("Recording completed to file {0}", targetFile); } - private EncodingOptions GetEncodingOptions() - { - return _config.GetConfiguration("encoding"); - } - private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { _targetPath = targetFile; @@ -108,7 +98,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV StartInfo = processStartInfo, EnableRaisingEvents = true }; - _process.Exited += (sender, args) => OnFfMpegProcessExited(_process, inputFile); + _process.Exited += (sender, args) => OnFfMpegProcessExited(_process); _process.Start(); @@ -221,20 +211,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } protected string GetOutputSizeParam() - { - var filters = new List(); - - filters.Add("yadif=0:-1:0"); - - var output = string.Empty; - - if (filters.Count > 0) - { - output += string.Format(CultureInfo.InvariantCulture, " -vf \"{0}\"", string.Join(",", filters.ToArray())); - } - - return output; - } + => "-vf \"yadif=0:-1:0\""; private void Stop() { @@ -291,7 +268,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV /// /// Processes the exited. /// - private void OnFfMpegProcessExited(Process process, string inputFile) + private void OnFfMpegProcessExited(Process process) { using (process) { diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index c4d5cc58a..33331adaf 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -24,14 +24,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings { public class SchedulesDirect : IListingsProvider { + private const string ApiUrl = "https://json.schedulesdirect.org/20141201"; + private readonly ILogger _logger; private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClient _httpClient; private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1); private readonly IApplicationHost _appHost; - private const string ApiUrl = "https://json.schedulesdirect.org/20141201"; - public SchedulesDirect( ILogger logger, IJsonSerializer jsonSerializer, @@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings while (start <= end) { - dates.Add(start.ToString("yyyy-MM-dd")); + dates.Add(start.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); start = start.AddDays(1); } @@ -367,13 +367,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings if (!string.IsNullOrWhiteSpace(details.originalAirDate)) { - info.OriginalAirDate = DateTime.Parse(details.originalAirDate); + info.OriginalAirDate = DateTime.Parse(details.originalAirDate, CultureInfo.InvariantCulture); info.ProductionYear = info.OriginalAirDate.Value.Year; } if (details.movie != null) { - if (!string.IsNullOrEmpty(details.movie.year) && int.TryParse(details.movie.year, out int year)) + if (!string.IsNullOrEmpty(details.movie.year) + && int.TryParse(details.movie.year, out int year)) { info.ProductionYear = year; } @@ -587,7 +588,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings return null; } - NameValuePair savedToken = null; + NameValuePair savedToken; if (!_tokens.TryGetValue(username, out savedToken)) { savedToken = new NameValuePair(); @@ -633,7 +634,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings } } - private async Task Post(HttpRequestOptions options, + private async Task Post( + HttpRequestOptions options, bool enableRetry, ListingsProviderInfo providerInfo) { @@ -663,7 +665,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings return await Post(options, false, providerInfo).ConfigureAwait(false); } - private async Task Get(HttpRequestOptions options, + private async Task Get( + HttpRequestOptions options, bool enableRetry, ListingsProviderInfo providerInfo) { @@ -693,7 +696,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings return await Get(options, false, providerInfo).ConfigureAwait(false); } - private async Task GetTokenInternal(string username, string password, + private async Task GetTokenInternal( + string username, + string password, CancellationToken cancellationToken) { var httpOptions = new HttpRequestOptions() diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs index e29fcfb5f..5adcefc1f 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs @@ -5,10 +5,10 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -using MediaBrowser.Model.Globalization; namespace Emby.Server.Implementations.ScheduledTasks.Tasks { @@ -21,10 +21,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// Gets or sets the application paths. /// /// The application paths. - private IApplicationPaths ApplicationPaths { get; set; } - + private readonly IApplicationPaths _applicationPaths; private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; private readonly ILocalizationManager _localization; @@ -37,20 +35,41 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks IFileSystem fileSystem, ILocalizationManager localization) { - ApplicationPaths = appPaths; + _applicationPaths = appPaths; _logger = logger; _fileSystem = fileSystem; _localization = localization; } + /// + public string Name => _localization.GetLocalizedString("TaskCleanCache"); + + /// + public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription"); + + /// + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + + /// + public string Key => "DeleteCacheFiles"; + + /// + public bool IsHidden => false; + + /// + public bool IsEnabled => true; + + /// + public bool IsLogged => true; + /// /// Creates the triggers that define when the task will run. /// /// IEnumerable{BaseTaskTrigger}. public IEnumerable GetDefaultTriggers() { - return new[] { - + return new[] + { // Every so often new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} }; @@ -68,7 +87,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks try { - DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.CachePath, minDateModified, progress); + DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.CachePath, minDateModified, progress); } catch (DirectoryNotFoundException) { @@ -81,7 +100,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks try { - DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.TempDirectory, minDateModified, progress); + DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.TempDirectory, minDateModified, progress); } catch (DirectoryNotFoundException) { @@ -91,7 +110,6 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks return Task.CompletedTask; } - /// /// Deletes the cache files from directory with a last write time less than a given date. /// @@ -164,26 +182,5 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks _logger.LogError(ex, "Error deleting file {path}", path); } } - - /// - public string Name => _localization.GetLocalizedString("TaskCleanCache"); - - /// - public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription"); - - /// - public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); - - /// - public string Key => "DeleteCacheFiles"; - - /// - public bool IsHidden => false; - - /// - public bool IsEnabled => true; - - /// - public bool IsLogged => true; } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs index 7388086fb..c5af68bce 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs @@ -34,6 +34,27 @@ namespace Emby.Server.Implementations.ScheduledTasks _localization = localization; } + /// + public string Name => _localization.GetLocalizedString("TaskUpdatePlugins"); + + /// + public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription"); + + /// + public string Category => _localization.GetLocalizedString("TasksApplicationCategory"); + + /// + public string Key => "PluginUpdates"; + + /// + public bool IsHidden => false; + + /// + public bool IsEnabled => true; + + /// + public bool IsLogged => true; + /// /// Creates the triggers that define when the task will run. /// @@ -98,26 +119,5 @@ namespace Emby.Server.Implementations.ScheduledTasks progress.Report(100); } - - /// - public string Name => _localization.GetLocalizedString("TaskUpdatePlugins"); - - /// - public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription"); - - /// - public string Category => _localization.GetLocalizedString("TasksApplicationCategory"); - - /// - public string Key => "PluginUpdates"; - - /// - public bool IsHidden => false; - - /// - public bool IsEnabled => true; - - /// - public bool IsLogged => true; } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs index eb628ec5f..8b67d37d7 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs @@ -11,7 +11,12 @@ namespace Emby.Server.Implementations.ScheduledTasks public class DailyTrigger : ITaskTrigger { /// - /// Get the time of day to trigger the task to run. + /// Occurs when [triggered]. + /// + public event EventHandler Triggered; + + /// + /// Gets or sets the time of day to trigger the task to run. /// /// The time of day. public TimeSpan TimeOfDay { get; set; } @@ -69,11 +74,6 @@ namespace Emby.Server.Implementations.ScheduledTasks } } - /// - /// Occurs when [triggered]. - /// - public event EventHandler Triggered; - /// /// Called when [triggered]. /// diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs index 247a6785a..b04fd7c7e 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs @@ -11,6 +11,13 @@ namespace Emby.Server.Implementations.ScheduledTasks /// public class IntervalTrigger : ITaskTrigger { + private DateTime _lastStartDate; + + /// + /// Occurs when [triggered]. + /// + public event EventHandler Triggered; + /// /// Gets or sets the interval. /// @@ -28,8 +35,6 @@ namespace Emby.Server.Implementations.ScheduledTasks /// The timer. private Timer Timer { get; set; } - private DateTime _lastStartDate; - /// /// Stars waiting for the trigger action. /// @@ -88,11 +93,6 @@ namespace Emby.Server.Implementations.ScheduledTasks } } - /// - /// Occurs when [triggered]. - /// - public event EventHandler Triggered; - /// /// Called when [triggered]. /// diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs index 96e5d8897..7cd5493da 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs @@ -12,6 +12,11 @@ namespace Emby.Server.Implementations.ScheduledTasks /// public class StartupTrigger : ITaskTrigger { + /// + /// Occurs when [triggered]. + /// + public event EventHandler Triggered; + public int DelayMs { get; set; } /// @@ -48,20 +53,12 @@ namespace Emby.Server.Implementations.ScheduledTasks { } - /// - /// Occurs when [triggered]. - /// - public event EventHandler Triggered; - /// /// Called when [triggered]. /// private void OnTriggered() { - if (Triggered != null) - { - Triggered(this, EventArgs.Empty); - } + Triggered?.Invoke(this, EventArgs.Empty); } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs index 4f1bf5c19..0c0ebec08 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs @@ -11,7 +11,12 @@ namespace Emby.Server.Implementations.ScheduledTasks public class WeeklyTrigger : ITaskTrigger { /// - /// Get the time of day to trigger the task to run. + /// Occurs when [triggered]. + /// + public event EventHandler Triggered; + + /// + /// Gets or sets the time of day to trigger the task to run. /// /// The time of day. public TimeSpan TimeOfDay { get; set; } @@ -95,20 +100,12 @@ namespace Emby.Server.Implementations.ScheduledTasks } } - /// - /// Occurs when [triggered]. - /// - public event EventHandler Triggered; - /// /// Called when [triggered]. /// private void OnTriggered() { - if (Triggered != null) - { - Triggered(this, EventArgs.Empty); - } + Triggered?.Invoke(this, EventArgs.Empty); } } } diff --git a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs index d7afd2118..44bd38b54 100644 --- a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs +++ b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs @@ -62,6 +62,7 @@ namespace MediaBrowser.Controller.LiveTv /// /// null if [has image] contains no value, true if [has image]; otherwise, false. public bool? HasImage { get; set; } + /// /// Gets or sets a value indicating whether this instance is favorite. /// diff --git a/MediaBrowser.Model/IO/FileSystemMetadata.cs b/MediaBrowser.Model/IO/FileSystemMetadata.cs index b23119d08..118c78e80 100644 --- a/MediaBrowser.Model/IO/FileSystemMetadata.cs +++ b/MediaBrowser.Model/IO/FileSystemMetadata.cs @@ -56,7 +56,7 @@ namespace MediaBrowser.Model.IO public DateTime CreationTimeUtc { get; set; } /// - /// Gets a value indicating whether this instance is directory. + /// Gets or sets a value indicating whether this instance is directory. /// /// true if this instance is directory; otherwise, false. public bool IsDirectory { get; set; } diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs index bba69d4b4..dc6549787 100644 --- a/MediaBrowser.Model/IO/IFileSystem.cs +++ b/MediaBrowser.Model/IO/IFileSystem.cs @@ -201,9 +201,9 @@ namespace MediaBrowser.Model.IO IEnumerable GetFileSystemEntryPaths(string path, bool recursive = false); void SetHidden(string path, bool isHidden); - void SetReadOnly(string path, bool readOnly); + void SetAttributes(string path, bool isHidden, bool readOnly); + List GetDrives(); - void SetExecutable(string path); } } diff --git a/MediaBrowser.Model/IO/IShortcutHandler.cs b/MediaBrowser.Model/IO/IShortcutHandler.cs index 5c663aa0d..14d5c4b62 100644 --- a/MediaBrowser.Model/IO/IShortcutHandler.cs +++ b/MediaBrowser.Model/IO/IShortcutHandler.cs @@ -22,7 +22,6 @@ namespace MediaBrowser.Model.IO /// /// The shortcut path. /// The target path. - /// System.String. void Create(string shortcutPath, string targetPath); } } diff --git a/MediaBrowser.Model/IO/IStreamHelper.cs b/MediaBrowser.Model/IO/IStreamHelper.cs index af5ba5b17..0e09db16e 100644 --- a/MediaBrowser.Model/IO/IStreamHelper.cs +++ b/MediaBrowser.Model/IO/IStreamHelper.cs @@ -13,8 +13,6 @@ namespace MediaBrowser.Model.IO Task CopyToAsync(Stream source, Stream destination, int bufferSize, int emptyReadLimit, CancellationToken cancellationToken); - Task CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken); - Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken); Task CopyUntilCancelled(Stream source, Stream target, int bufferSize, CancellationToken cancellationToken); diff --git a/MediaBrowser.Model/IO/IZipClient.cs b/MediaBrowser.Model/IO/IZipClient.cs index 2daa54f22..fca52ebae 100644 --- a/MediaBrowser.Model/IO/IZipClient.cs +++ b/MediaBrowser.Model/IO/IZipClient.cs @@ -26,6 +26,7 @@ namespace MediaBrowser.Model.IO void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles); void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles); + void ExtractFirstFileFromGz(Stream source, string targetPath, string defaultFileName); /// diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs index f31a7faea..291b36027 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs @@ -31,9 +31,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People _httpClientFactory = httpClientFactory; } + public static string ProviderName => TmdbUtils.ProviderName; + + /// public string Name => ProviderName; - public static string ProviderName => TmdbUtils.ProviderName; + /// + public int Order => 0; public bool Supports(BaseItem item) { @@ -125,8 +129,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People return profile.Iso_639_1?.ToString(); } - public int Order => 0; - public Task GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs index e9fb5c703..a4b1387d3 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs @@ -32,7 +32,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People { const string DataFileName = "info.json"; - internal static TmdbPersonProvider Current { get; private set; } + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly IJsonSerializer _jsonSerializer; private readonly IFileSystem _fileSystem; @@ -55,6 +55,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People Current = this; } + internal static TmdbPersonProvider Current { get; private set; } + public string Name => TmdbUtils.ProviderName; public async Task> GetSearchResults(PersonLookupInfo searchInfo, CancellationToken cancellationToken) @@ -95,7 +97,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People return new List(); } - var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/search/person?api_key={1}&query={0}", WebUtility.UrlEncode(searchInfo.Name), TmdbUtils.ApiKey); + var url = string.Format( + CultureInfo.InvariantCulture, + TmdbUtils.BaseTmdbApiUrl + @"3/search/person?api_key={1}&query={0}", + WebUtility.UrlEncode(searchInfo.Name), + TmdbUtils.ApiKey); using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); foreach (var header in TmdbUtils.AcceptHeaders) @@ -200,8 +206,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People return result; } - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - /// /// Gets the TMDB id. /// @@ -226,7 +230,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People return; } - var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/person/{1}?api_key={0}&append_to_response=credits,images,external_ids", TmdbUtils.ApiKey, id); + var url = string.Format( + CultureInfo.InvariantCulture, + TmdbUtils.BaseTmdbApiUrl + @"3/person/{1}?api_key={0}&append_to_response=credits,images,external_ids", + TmdbUtils.ApiKey, + id); using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); foreach (var header in TmdbUtils.AcceptHeaders) -- cgit v1.2.3 From 4038d15c83da055add3cc78df46969f82ee66d1b Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 1 Sep 2020 07:51:06 -0600 Subject: Properly migrate all HttpCompletionOption --- Emby.Dlna/Eventing/DlnaEventManager.cs | 2 +- Emby.Dlna/PlayTo/SsdpHttpClient.cs | 6 +++--- Emby.Server.Implementations/ApplicationHost.cs | 2 +- Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs | 2 +- Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs | 9 +++++---- .../LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs | 6 +++--- .../LiveTv/TunerHosts/SharedHttpStream.cs | 2 +- 7 files changed, 15 insertions(+), 14 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs index 0c7591494..7d8da86ef 100644 --- a/Emby.Dlna/Eventing/DlnaEventManager.cs +++ b/Emby.Dlna/Eventing/DlnaEventManager.cs @@ -178,7 +178,7 @@ namespace Emby.Dlna.Eventing try { using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .SendAsync(options).ConfigureAwait(false); + .SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); } catch (OperationCanceledException) { diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs index c6e9e074f..65f1aab22 100644 --- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs +++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs @@ -84,7 +84,7 @@ namespace Emby.Dlna.PlayTo options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(_usCulture)); using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .SendAsync(options) + .SendAsync(options, HttpCompletionOption.ResponseHeadersRead) .ConfigureAwait(false); } @@ -93,7 +93,7 @@ namespace Emby.Dlna.PlayTo using var options = new HttpRequestMessage(HttpMethod.Get, url); options.Headers.UserAgent.Add(ProductInfoHeaderValue.Parse(USERAGENT)); options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName); - using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, cancellationToken).ConfigureAwait(false); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); using var reader = new StreamReader(stream, Encoding.UTF8); return XDocument.Parse( @@ -126,7 +126,7 @@ namespace Emby.Dlna.PlayTo options.Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml); - return _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, cancellationToken); + return _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken); } } } diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index fbf4aef8b..006060079 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1299,7 +1299,7 @@ namespace Emby.Server.Implementations { using var request = new HttpRequestMessage(HttpMethod.Post, apiUrl); using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .SendAsync(request, cancellationToken).ConfigureAwait(false); + .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); var result = await System.Text.Json.JsonSerializer.DeserializeAsync(stream, JsonDefaults.GetOptions(), cancellationToken).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs index 377292cab..a1037948b 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private async Task RecordFromMediaSource(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .GetAsync(mediaSource.Path, cancellationToken).ConfigureAwait(false); + .GetAsync(mediaSource.Path, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); _logger.LogInformation("Opened recording stream from tuner provider"); diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 439e50278..f426a3336 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -607,11 +607,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings HttpRequestMessage options, bool enableRetry, ListingsProviderInfo providerInfo, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { try { - return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, cancellationToken).ConfigureAwait(false); + return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false); } catch (HttpException ex) { @@ -670,7 +671,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings using var options = new HttpRequestMessage(HttpMethod.Put, ApiUrl + "/lineups/" + info.ListingsId); options.Headers.TryAddWithoutValidation("token", token); - using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, cancellationToken).ConfigureAwait(false); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); } private async Task HasLineup(ListingsProviderInfo info, CancellationToken cancellationToken) @@ -694,7 +695,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings try { - using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false); + using var httpResponse = await Send(options, false, null, cancellationToken, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); await using var stream = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); using var response = httpResponse.Content; var root = await _jsonSerializer.DeserializeFromStreamAsync(stream).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 418e27eec..1873d1789 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun BufferContent = false }; - using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, cancellationToken).ConfigureAwait(false); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(); var lineup = await JsonSerializer.DeserializeAsync>(stream, cancellationToken: cancellationToken) .ConfigureAwait(false) ?? new List(); @@ -134,7 +134,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun try { using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)), cancellationToken) + .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); var discoverResponse = await JsonSerializer.DeserializeAsync(stream, cancellationToken: cancellationToken) @@ -180,7 +180,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), cancellationToken) + .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); using var sr = new StreamReader(stream, System.Text.Encoding.UTF8); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index 4c9722b29..7a16704a9 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -69,7 +69,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts } using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .GetAsync(url, CancellationToken.None) + .GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) .ConfigureAwait(false); var extension = "ts"; -- cgit v1.2.3 From e3377564288598742dbf64f396ed38e42b6b2915 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Wed, 2 Sep 2020 12:22:14 +0200 Subject: Remove ServiceStack and related stuff --- Emby.Server.Implementations/ApplicationHost.cs | 8 +- .../HttpServer/FileWriter.cs | 250 ------- .../HttpServer/HttpListenerHost.cs | 233 +------ .../HttpServer/HttpResultFactory.cs | 721 --------------------- .../HttpServer/RangeRequestWriter.cs | 212 ------ .../HttpServer/ResponseFilter.cs | 113 ---- .../HttpServer/Security/AuthService.cs | 213 +----- .../HttpServer/Security/AuthorizationContext.cs | 21 +- .../HttpServer/Security/SessionContext.cs | 20 +- .../HttpServer/StreamWriter.cs | 120 ---- Emby.Server.Implementations/Services/HttpResult.cs | 64 -- .../Services/RequestHelper.cs | 51 -- .../Services/ResponseHelper.cs | 141 ---- .../Services/ServiceController.cs | 202 ------ .../Services/ServiceExec.cs | 230 ------- .../Services/ServiceHandler.cs | 212 ------ .../Services/ServiceMethod.cs | 20 - .../Services/ServicePath.cs | 550 ---------------- .../Services/StringMapTypeDeserializer.cs | 118 ---- .../Services/UrlExtensions.cs | 27 - .../SocketSharp/WebSocketSharpRequest.cs | 248 ------- .../Extensions/HttpContextExtensions.cs | 55 +- .../MediaEncoding/EncodingJobOptions.cs | 30 - .../Net/AuthenticatedAttribute.cs | 76 --- MediaBrowser.Controller/Net/IAuthService.cs | 17 - .../Net/IAuthorizationContext.cs | 3 +- MediaBrowser.Controller/Net/IHasResultFactory.cs | 17 - MediaBrowser.Controller/Net/IHttpResultFactory.cs | 82 --- MediaBrowser.Controller/Net/IHttpServer.cs | 9 +- MediaBrowser.Controller/Net/ISessionContext.cs | 6 +- MediaBrowser.Controller/Net/StaticResultOptions.cs | 44 -- MediaBrowser.Model/Services/ApiMemberAttribute.cs | 65 -- MediaBrowser.Model/Services/IAsyncStreamWriter.cs | 13 - MediaBrowser.Model/Services/IHasHeaders.cs | 11 - MediaBrowser.Model/Services/IHasRequestFilter.cs | 24 - MediaBrowser.Model/Services/IHttpRequest.cs | 17 - MediaBrowser.Model/Services/IHttpResult.cs | 35 - MediaBrowser.Model/Services/IRequest.cs | 93 --- .../Services/IRequiresRequestStream.cs | 14 - MediaBrowser.Model/Services/IService.cs | 15 - MediaBrowser.Model/Services/IStreamWriter.cs | 11 - .../Services/QueryParamCollection.cs | 147 ----- MediaBrowser.Model/Services/RouteAttribute.cs | 163 ----- MediaBrowser.Model/Session/PlayRequest.cs | 1 - .../HttpServer/ResponseFilterTests.cs | 18 - 45 files changed, 91 insertions(+), 4649 deletions(-) delete mode 100644 Emby.Server.Implementations/HttpServer/FileWriter.cs delete mode 100644 Emby.Server.Implementations/HttpServer/HttpResultFactory.cs delete mode 100644 Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs delete mode 100644 Emby.Server.Implementations/HttpServer/ResponseFilter.cs delete mode 100644 Emby.Server.Implementations/HttpServer/StreamWriter.cs delete mode 100644 Emby.Server.Implementations/Services/HttpResult.cs delete mode 100644 Emby.Server.Implementations/Services/RequestHelper.cs delete mode 100644 Emby.Server.Implementations/Services/ResponseHelper.cs delete mode 100644 Emby.Server.Implementations/Services/ServiceController.cs delete mode 100644 Emby.Server.Implementations/Services/ServiceExec.cs delete mode 100644 Emby.Server.Implementations/Services/ServiceHandler.cs delete mode 100644 Emby.Server.Implementations/Services/ServiceMethod.cs delete mode 100644 Emby.Server.Implementations/Services/ServicePath.cs delete mode 100644 Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs delete mode 100644 Emby.Server.Implementations/Services/UrlExtensions.cs delete mode 100644 Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs delete mode 100644 MediaBrowser.Controller/Net/AuthenticatedAttribute.cs delete mode 100644 MediaBrowser.Controller/Net/IHasResultFactory.cs delete mode 100644 MediaBrowser.Controller/Net/IHttpResultFactory.cs delete mode 100644 MediaBrowser.Controller/Net/StaticResultOptions.cs delete mode 100644 MediaBrowser.Model/Services/ApiMemberAttribute.cs delete mode 100644 MediaBrowser.Model/Services/IAsyncStreamWriter.cs delete mode 100644 MediaBrowser.Model/Services/IHasHeaders.cs delete mode 100644 MediaBrowser.Model/Services/IHasRequestFilter.cs delete mode 100644 MediaBrowser.Model/Services/IHttpRequest.cs delete mode 100644 MediaBrowser.Model/Services/IHttpResult.cs delete mode 100644 MediaBrowser.Model/Services/IRequest.cs delete mode 100644 MediaBrowser.Model/Services/IRequiresRequestStream.cs delete mode 100644 MediaBrowser.Model/Services/IService.cs delete mode 100644 MediaBrowser.Model/Services/IStreamWriter.cs delete mode 100644 MediaBrowser.Model/Services/QueryParamCollection.cs delete mode 100644 MediaBrowser.Model/Services/RouteAttribute.cs delete mode 100644 tests/Jellyfin.Server.Implementations.Tests/HttpServer/ResponseFilterTests.cs (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index e9b063277..4f47d1999 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -41,7 +41,6 @@ using Emby.Server.Implementations.QuickConnect; using Emby.Server.Implementations.ScheduledTasks; using Emby.Server.Implementations.Security; using Emby.Server.Implementations.Serialization; -using Emby.Server.Implementations.Services; using Emby.Server.Implementations.Session; using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.TV; @@ -90,7 +89,6 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Net; using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Services; using MediaBrowser.Model.System; using MediaBrowser.Model.Tasks; using MediaBrowser.Providers.Chapters; @@ -544,8 +542,6 @@ namespace Emby.Server.Implementations ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(this); ServiceCollection.AddSingleton(ApplicationPaths); @@ -581,7 +577,6 @@ namespace Emby.Server.Implementations ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); ServiceCollection.AddSingleton(); ServiceCollection.AddSingleton(); @@ -757,7 +752,6 @@ namespace Emby.Server.Implementations CollectionFolder.XmlSerializer = _xmlSerializer; CollectionFolder.JsonSerializer = Resolve(); CollectionFolder.ApplicationHost = this; - AuthenticatedAttribute.AuthService = Resolve(); } /// @@ -777,7 +771,7 @@ namespace Emby.Server.Implementations .Where(i => i != null) .ToArray(); - _httpServer.Init(GetExportTypes(), GetExports(), GetUrlPrefixes()); + _httpServer.Init(GetExports(), GetUrlPrefixes()); Resolve().AddParts( GetExports(), diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs deleted file mode 100644 index 6fce8de44..000000000 --- a/Emby.Server.Implementations/HttpServer/FileWriter.cs +++ /dev/null @@ -1,250 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; - -namespace Emby.Server.Implementations.HttpServer -{ - public class FileWriter : IHttpResult - { - private static readonly CultureInfo UsCulture = CultureInfo.ReadOnly(new CultureInfo("en-US")); - - private static readonly string[] _skipLogExtensions = { - ".js", - ".html", - ".css" - }; - - private readonly IStreamHelper _streamHelper; - private readonly ILogger _logger; - - /// - /// The _options. - /// - private readonly IDictionary _options = new Dictionary(); - - /// - /// The _requested ranges. - /// - private List> _requestedRanges; - - public FileWriter(string path, string contentType, string rangeHeader, ILogger logger, IFileSystem fileSystem, IStreamHelper streamHelper) - { - if (string.IsNullOrEmpty(contentType)) - { - throw new ArgumentNullException(nameof(contentType)); - } - - _streamHelper = streamHelper; - - Path = path; - _logger = logger; - RangeHeader = rangeHeader; - - Headers[HeaderNames.ContentType] = contentType; - - TotalContentLength = fileSystem.GetFileInfo(path).Length; - Headers[HeaderNames.AcceptRanges] = "bytes"; - - if (string.IsNullOrWhiteSpace(rangeHeader)) - { - Headers[HeaderNames.ContentLength] = TotalContentLength.ToString(CultureInfo.InvariantCulture); - StatusCode = HttpStatusCode.OK; - } - else - { - StatusCode = HttpStatusCode.PartialContent; - SetRangeValues(); - } - - FileShare = FileShare.Read; - Cookies = new List(); - } - - private string RangeHeader { get; set; } - - private bool IsHeadRequest { get; set; } - - private long RangeStart { get; set; } - - private long RangeEnd { get; set; } - - private long RangeLength { get; set; } - - public long TotalContentLength { get; set; } - - public Action OnComplete { get; set; } - - public Action OnError { get; set; } - - public List Cookies { get; private set; } - - public FileShare FileShare { get; set; } - - /// - /// Gets the options. - /// - /// The options. - public IDictionary Headers => _options; - - public string Path { get; set; } - - /// - /// Gets the requested ranges. - /// - /// The requested ranges. - protected List> RequestedRanges - { - get - { - if (_requestedRanges == null) - { - _requestedRanges = new List>(); - - // Example: bytes=0-,32-63 - var ranges = RangeHeader.Split('=')[1].Split(','); - - foreach (var range in ranges) - { - var vals = range.Split('-'); - - long start = 0; - long? end = null; - - if (!string.IsNullOrEmpty(vals[0])) - { - start = long.Parse(vals[0], UsCulture); - } - - if (!string.IsNullOrEmpty(vals[1])) - { - end = long.Parse(vals[1], UsCulture); - } - - _requestedRanges.Add(new KeyValuePair(start, end)); - } - } - - return _requestedRanges; - } - } - - public string ContentType { get; set; } - - public IRequest RequestContext { get; set; } - - public object Response { get; set; } - - public int Status { get; set; } - - public HttpStatusCode StatusCode - { - get => (HttpStatusCode)Status; - set => Status = (int)value; - } - - /// - /// Sets the range values. - /// - private void SetRangeValues() - { - var requestedRange = RequestedRanges[0]; - - // If the requested range is "0-", we can optimize by just doing a stream copy - if (!requestedRange.Value.HasValue) - { - RangeEnd = TotalContentLength - 1; - } - else - { - RangeEnd = requestedRange.Value.Value; - } - - RangeStart = requestedRange.Key; - RangeLength = 1 + RangeEnd - RangeStart; - - // Content-Length is the length of what we're serving, not the original content - var lengthString = RangeLength.ToString(CultureInfo.InvariantCulture); - Headers[HeaderNames.ContentLength] = lengthString; - var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; - Headers[HeaderNames.ContentRange] = rangeString; - - _logger.LogDebug("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString); - } - - public async Task WriteToAsync(HttpResponse response, CancellationToken cancellationToken) - { - try - { - // Headers only - if (IsHeadRequest) - { - return; - } - - var path = Path; - var offset = RangeStart; - var count = RangeLength; - - if (string.IsNullOrWhiteSpace(RangeHeader) || RangeStart <= 0 && RangeEnd >= TotalContentLength - 1) - { - var extension = System.IO.Path.GetExtension(path); - - if (extension == null || !_skipLogExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) - { - _logger.LogDebug("Transmit file {0}", path); - } - - offset = 0; - count = 0; - } - - await TransmitFile(response.Body, path, offset, count, FileShare, cancellationToken).ConfigureAwait(false); - } - finally - { - OnComplete?.Invoke(); - } - } - - public async Task TransmitFile(Stream stream, string path, long offset, long count, FileShare fileShare, CancellationToken cancellationToken) - { - var fileOptions = FileOptions.SequentialScan; - - // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - fileOptions |= FileOptions.Asynchronous; - } - - using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, fileOptions)) - { - if (offset > 0) - { - fs.Position = offset; - } - - if (count > 0) - { - await _streamHelper.CopyToAsync(fs, stream, count, cancellationToken).ConfigureAwait(false); - } - else - { - await fs.CopyToAsync(stream, IODefaults.CopyToBufferSize, cancellationToken).ConfigureAwait(false); - } - } - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index fe39bb4b2..30cb7dd3a 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -7,11 +7,8 @@ using System.IO; using System.Linq; using System.Net.Sockets; using System.Net.WebSockets; -using System.Reflection; using System.Threading; using System.Threading.Tasks; -using Emby.Server.Implementations.Services; -using Emby.Server.Implementations.SocketSharp; using Jellyfin.Data.Events; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; @@ -20,8 +17,6 @@ using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.WebUtilities; @@ -29,7 +24,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; -using ServiceStack.Text.Jsv; namespace Emby.Server.Implementations.HttpServer { @@ -46,13 +40,9 @@ namespace Emby.Server.Implementations.HttpServer private readonly IServerConfigurationManager _config; private readonly INetworkManager _networkManager; private readonly IServerApplicationHost _appHost; - private readonly IJsonSerializer _jsonSerializer; - private readonly IXmlSerializer _xmlSerializer; - private readonly Func> _funcParseFn; private readonly string _defaultRedirectPath; private readonly string _baseUrlPrefix; - private readonly Dictionary _serviceOperationsMap = new Dictionary(); private readonly IHostEnvironment _hostEnvironment; private IWebSocketListener[] _webSocketListeners = Array.Empty(); @@ -64,10 +54,7 @@ namespace Emby.Server.Implementations.HttpServer IServerConfigurationManager config, IConfiguration configuration, INetworkManager networkManager, - IJsonSerializer jsonSerializer, - IXmlSerializer xmlSerializer, ILocalizationManager localizationManager, - ServiceController serviceController, IHostEnvironment hostEnvironment, ILoggerFactory loggerFactory) { @@ -77,36 +64,21 @@ namespace Emby.Server.Implementations.HttpServer _defaultRedirectPath = configuration[DefaultRedirectKey]; _baseUrlPrefix = _config.Configuration.BaseUrl; _networkManager = networkManager; - _jsonSerializer = jsonSerializer; - _xmlSerializer = xmlSerializer; - ServiceController = serviceController; _hostEnvironment = hostEnvironment; _loggerFactory = loggerFactory; - _funcParseFn = t => s => JsvReader.GetParseFn(t)(s); - Instance = this; - ResponseFilters = Array.Empty>(); GlobalResponse = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); } public event EventHandler> WebSocketConnected; - public Action[] ResponseFilters { get; set; } - public static HttpListenerHost Instance { get; protected set; } public string[] UrlPrefixes { get; private set; } public string GlobalResponse { get; set; } - public ServiceController ServiceController { get; } - - public object CreateInstance(Type type) - { - return _appHost.CreateInstance(type); - } - private static string NormalizeUrlPath(string path) { if (path.Length > 0 && path[0] == '/') @@ -121,58 +93,6 @@ namespace Emby.Server.Implementations.HttpServer } } - /// - /// Applies the request filters. Returns whether or not the request has been handled - /// and no more processing should be done. - /// - /// - public void ApplyRequestFilters(IRequest req, HttpResponse res, object requestDto) - { - // Exec all RequestFilter attributes with Priority < 0 - var attributes = GetRequestFilterAttributes(requestDto.GetType()); - - int count = attributes.Count; - int i = 0; - for (; i < count && attributes[i].Priority < 0; i++) - { - var attribute = attributes[i]; - attribute.RequestFilter(req, res, requestDto); - } - - // Exec remaining RequestFilter attributes with Priority >= 0 - for (; i < count && attributes[i].Priority >= 0; i++) - { - var attribute = attributes[i]; - attribute.RequestFilter(req, res, requestDto); - } - } - - public Type GetServiceTypeByRequest(Type requestType) - { - _serviceOperationsMap.TryGetValue(requestType, out var serviceType); - return serviceType; - } - - public void AddServiceInfo(Type serviceType, Type requestType) - { - _serviceOperationsMap[requestType] = serviceType; - } - - private List GetRequestFilterAttributes(Type requestDtoType) - { - var attributes = requestDtoType.GetCustomAttributes(true).OfType().ToList(); - - var serviceType = GetServiceTypeByRequest(requestDtoType); - if (serviceType != null) - { - attributes.AddRange(serviceType.GetCustomAttributes(true).OfType()); - } - - attributes.Sort((x, y) => x.Priority - y.Priority); - - return attributes; - } - private static Exception GetActualException(Exception ex) { if (ex is AggregateException agg) @@ -210,7 +130,7 @@ namespace Emby.Server.Implementations.HttpServer } } - private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog, bool ignoreStackTrace) + private async Task ErrorHandler(Exception ex, HttpContext httpContext, int statusCode, string urlToLog, bool ignoreStackTrace) { if (ignoreStackTrace) { @@ -221,7 +141,7 @@ namespace Emby.Server.Implementations.HttpServer _logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog); } - var httpRes = httpReq.Response; + var httpRes = httpContext.Response; if (httpRes.HasStarted) { @@ -395,24 +315,22 @@ namespace Emby.Server.Implementations.HttpServer 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); - return RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted); + return RequestHandler(context, context.RequestAborted); } /// /// Overridable method that can be used to implement a custom handler. /// - private async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken) + private async Task RequestHandler(HttpContext httpContext, CancellationToken cancellationToken) { var stopWatch = new Stopwatch(); stopWatch.Start(); - var httpRes = httpReq.Response; + var httpRes = httpContext.Response; + var host = httpContext.Request.Host.ToString(); + var localPath = httpContext.Request.Path.ToString(); + var urlString = httpContext.Request.GetDisplayUrl(); string urlToLog = GetUrlToLog(urlString); - string remoteIp = httpReq.RemoteIp; + string remoteIp = httpContext.Request.RemoteIp(); try { @@ -432,7 +350,7 @@ namespace Emby.Server.Implementations.HttpServer return; } - if (!ValidateRequest(remoteIp, httpReq.IsLocal)) + if (!ValidateRequest(remoteIp, httpContext.Request.IsLocal())) { httpRes.StatusCode = 403; httpRes.ContentType = "text/plain"; @@ -440,16 +358,16 @@ namespace Emby.Server.Implementations.HttpServer return; } - if (!ValidateSsl(httpReq.RemoteIp, urlString)) + if (!ValidateSsl(httpContext.Request.RemoteIp(), urlString)) { - RedirectToSecureUrl(httpReq, httpRes, urlString); + RedirectToSecureUrl(httpRes, urlString); return; } - if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(httpContext.Request.Method, "OPTIONS", StringComparison.OrdinalIgnoreCase)) { httpRes.StatusCode = 200; - foreach (var (key, value) in GetDefaultCorsHeaders(httpReq)) + foreach (var (key, value) in GetDefaultCorsHeaders(httpContext)) { httpRes.Headers.Add(key, value); } @@ -483,15 +401,7 @@ namespace Emby.Server.Implementations.HttpServer } } - var handler = GetServiceHandler(httpReq); - if (handler != null) - { - await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false); - } - else - { - throw new FileNotFoundException(); - } + throw new FileNotFoundException(); } catch (Exception requestEx) { @@ -500,7 +410,7 @@ namespace Emby.Server.Implementations.HttpServer var requestInnerEx = GetActualException(requestEx); var statusCode = GetStatusCode(requestInnerEx); - foreach (var (key, value) in GetDefaultCorsHeaders(httpReq)) + foreach (var (key, value) in GetDefaultCorsHeaders(httpContext)) { if (!httpRes.Headers.ContainsKey(key)) { @@ -525,7 +435,7 @@ namespace Emby.Server.Implementations.HttpServer throw; } - await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false); + await ErrorHandler(requestInnerEx, httpContext, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false); } catch (Exception handlerException) { @@ -596,12 +506,12 @@ namespace Emby.Server.Implementations.HttpServer /// /// /// - public IDictionary GetDefaultCorsHeaders(IRequest req) + public IDictionary GetDefaultCorsHeaders(HttpContext httpContext) { - var origin = req.Headers["Origin"]; + var origin = httpContext.Request.Headers["Origin"]; if (origin == StringValues.Empty) { - origin = req.Headers["Host"]; + origin = httpContext.Request.Headers["Host"]; if (origin == StringValues.Empty) { origin = "*"; @@ -616,23 +526,7 @@ namespace Emby.Server.Implementations.HttpServer return headers; } - // Entry point for HttpListener - public ServiceHandler GetServiceHandler(IHttpRequest httpReq) - { - var pathInfo = httpReq.PathInfo; - - pathInfo = ServiceHandler.GetSanitizedPathInfo(pathInfo, out string contentType); - var restPath = ServiceController.GetRestPathForRequest(httpReq.HttpMethod, pathInfo); - if (restPath != null) - { - return new ServiceHandler(restPath, contentType); - } - - _logger.LogError("Could not find handler for {PathInfo}", pathInfo); - return null; - } - - private void RedirectToSecureUrl(IHttpRequest httpReq, HttpResponse httpRes, string url) + private void RedirectToSecureUrl(HttpResponse httpRes, string url) { if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) { @@ -650,95 +544,12 @@ namespace Emby.Server.Implementations.HttpServer /// /// Adds the rest handlers. /// - /// The service types to register with the . /// The web socket listeners. /// The URL prefixes. See . - public void Init(IEnumerable serviceTypes, IEnumerable listeners, IEnumerable urlPrefixes) + public void Init(IEnumerable listeners, IEnumerable urlPrefixes) { _webSocketListeners = listeners.ToArray(); UrlPrefixes = urlPrefixes.ToArray(); - - ServiceController.Init(this, serviceTypes); - - ResponseFilters = new Action[] - { - new ResponseFilter(this, _logger).FilterResponse - }; - } - - public RouteAttribute[] GetRouteAttributes(Type requestType) - { - var routes = requestType.GetTypeInfo().GetCustomAttributes(true).ToList(); - var clone = routes.ToList(); - - foreach (var route in clone) - { - routes.Add(new RouteAttribute(NormalizeCustomRoutePath(route.Path), route.Verbs) - { - Notes = route.Notes, - Priority = route.Priority, - Summary = route.Summary - }); - - routes.Add(new RouteAttribute(NormalizeEmbyRoutePath(route.Path), route.Verbs) - { - Notes = route.Notes, - Priority = route.Priority, - Summary = route.Summary - }); - - routes.Add(new RouteAttribute(NormalizeMediaBrowserRoutePath(route.Path), route.Verbs) - { - Notes = route.Notes, - Priority = route.Priority, - Summary = route.Summary - }); - } - - return routes.ToArray(); - } - - public Func GetParseFn(Type propertyType) - { - return _funcParseFn(propertyType); - } - - public void SerializeToJson(object o, Stream stream) - { - _jsonSerializer.SerializeToStream(o, stream); - } - - public void SerializeToXml(object o, Stream stream) - { - _xmlSerializer.SerializeToStream(o, stream); - } - - public Task DeserializeXml(Type type, Stream stream) - { - return Task.FromResult(_xmlSerializer.DeserializeFromStream(type, stream)); - } - - public Task DeserializeJson(Type type, Stream stream) - { - return _jsonSerializer.DeserializeFromStreamAsync(stream, type); - } - - private string NormalizeEmbyRoutePath(string path) - { - _logger.LogDebug("Normalizing /emby route"); - return _baseUrlPrefix + "/emby" + NormalizeUrlPath(path); - } - - private string NormalizeMediaBrowserRoutePath(string path) - { - _logger.LogDebug("Normalizing /mediabrowser route"); - return _baseUrlPrefix + "/mediabrowser" + NormalizeUrlPath(path); - } - - private string NormalizeCustomRoutePath(string path) - { - _logger.LogDebug("Normalizing custom route {0}", path); - return _baseUrlPrefix + NormalizeUrlPath(path); } /// diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs deleted file mode 100644 index 688216373..000000000 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ /dev/null @@ -1,721 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.IO.Compression; -using System.Net; -using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; -using System.Xml; -using Emby.Server.Implementations.Services; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; -using IRequest = MediaBrowser.Model.Services.IRequest; -using MimeTypes = MediaBrowser.Model.Net.MimeTypes; - -namespace Emby.Server.Implementations.HttpServer -{ - /// - /// Class HttpResultFactory. - /// - public class HttpResultFactory : IHttpResultFactory - { - // Last-Modified and If-Modified-Since must follow strict date format, - // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since - private const string HttpDateFormat = "ddd, dd MMM yyyy HH:mm:ss \"GMT\""; - // We specifically use en-US culture because both day of week and month names require it - private static readonly CultureInfo _enUSculture = new CultureInfo("en-US", false); - - /// - /// The logger. - /// - private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; - private readonly IJsonSerializer _jsonSerializer; - private readonly IStreamHelper _streamHelper; - - /// - /// Initializes a new instance of the class. - /// - public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IStreamHelper streamHelper) - { - _fileSystem = fileSystem; - _jsonSerializer = jsonSerializer; - _streamHelper = streamHelper; - _logger = loggerfactory.CreateLogger(); - } - - /// - /// Gets the result. - /// - /// The request context. - /// The content. - /// Type of the content. - /// The response headers. - /// System.Object. - public object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary responseHeaders = null) - { - return GetHttpResult(requestContext, content, contentType, true, responseHeaders); - } - - public object GetResult(string content, string contentType, IDictionary responseHeaders = null) - { - return GetHttpResult(null, content, contentType, true, responseHeaders); - } - - public object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary responseHeaders = null) - { - return GetHttpResult(requestContext, content, contentType, true, responseHeaders); - } - - public object GetResult(IRequest requestContext, string content, string contentType, IDictionary responseHeaders = null) - { - return GetHttpResult(requestContext, content, contentType, true, responseHeaders); - } - - public object GetRedirectResult(string url) - { - var responseHeaders = new Dictionary(); - responseHeaders[HeaderNames.Location] = url; - - var result = new HttpResult(Array.Empty(), "text/plain", HttpStatusCode.Redirect); - - AddResponseHeaders(result, responseHeaders); - - return result; - } - - /// - /// Gets the HTTP result. - /// - private IHasHeaders GetHttpResult(IRequest requestContext, Stream content, string contentType, bool addCachePrevention, IDictionary responseHeaders = null) - { - var result = new StreamWriter(content, contentType); - - if (responseHeaders == null) - { - responseHeaders = new Dictionary(); - } - - if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out _)) - { - responseHeaders[HeaderNames.Expires] = "0"; - } - - AddResponseHeaders(result, responseHeaders); - - return result; - } - - /// - /// Gets the HTTP result. - /// - private IHasHeaders GetHttpResult(IRequest requestContext, byte[] content, string contentType, bool addCachePrevention, IDictionary responseHeaders = null) - { - string compressionType = null; - bool isHeadRequest = false; - - if (requestContext != null) - { - compressionType = GetCompressionType(requestContext, content, contentType); - isHeadRequest = string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase); - } - - IHasHeaders result; - if (string.IsNullOrEmpty(compressionType)) - { - var contentLength = content.Length; - - if (isHeadRequest) - { - content = Array.Empty(); - } - - result = new StreamWriter(content, contentType, contentLength); - } - else - { - result = GetCompressedResult(content, compressionType, responseHeaders, isHeadRequest, contentType); - } - - if (responseHeaders == null) - { - responseHeaders = new Dictionary(); - } - - if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _)) - { - responseHeaders[HeaderNames.Expires] = "0"; - } - - AddResponseHeaders(result, responseHeaders); - - return result; - } - - /// - /// Gets the HTTP result. - /// - private IHasHeaders GetHttpResult(IRequest requestContext, string content, string contentType, bool addCachePrevention, IDictionary responseHeaders = null) - { - IHasHeaders result; - - var bytes = Encoding.UTF8.GetBytes(content); - - var compressionType = requestContext == null ? null : GetCompressionType(requestContext, bytes, contentType); - - var isHeadRequest = requestContext == null ? false : string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase); - - if (string.IsNullOrEmpty(compressionType)) - { - var contentLength = bytes.Length; - - if (isHeadRequest) - { - bytes = Array.Empty(); - } - - result = new StreamWriter(bytes, contentType, contentLength); - } - else - { - result = GetCompressedResult(bytes, compressionType, responseHeaders, isHeadRequest, contentType); - } - - if (responseHeaders == null) - { - responseHeaders = new Dictionary(); - } - - if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _)) - { - responseHeaders[HeaderNames.Expires] = "0"; - } - - AddResponseHeaders(result, responseHeaders); - - return result; - } - - /// - /// Gets the optimized result. - /// - /// - public object GetResult(IRequest requestContext, T result, IDictionary responseHeaders = null) - where T : class - { - if (result == null) - { - throw new ArgumentNullException(nameof(result)); - } - - if (responseHeaders == null) - { - responseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - responseHeaders[HeaderNames.Expires] = "0"; - - return ToOptimizedResultInternal(requestContext, result, responseHeaders); - } - - private string GetCompressionType(IRequest request, byte[] content, string responseContentType) - { - if (responseContentType == null) - { - return null; - } - - // Per apple docs, hls manifests must be compressed - if (!responseContentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase) && - responseContentType.IndexOf("json", StringComparison.OrdinalIgnoreCase) == -1 && - responseContentType.IndexOf("javascript", StringComparison.OrdinalIgnoreCase) == -1 && - responseContentType.IndexOf("xml", StringComparison.OrdinalIgnoreCase) == -1 && - responseContentType.IndexOf("application/x-mpegURL", StringComparison.OrdinalIgnoreCase) == -1) - { - return null; - } - - if (content.Length < 1024) - { - return null; - } - - return GetCompressionType(request); - } - - private static string GetCompressionType(IRequest request) - { - var acceptEncoding = request.Headers[HeaderNames.AcceptEncoding].ToString(); - - if (!string.IsNullOrEmpty(acceptEncoding)) - { - // if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1) - // return "br"; - - if (acceptEncoding.Contains("deflate", StringComparison.OrdinalIgnoreCase)) - { - return "deflate"; - } - - if (acceptEncoding.Contains("gzip", StringComparison.OrdinalIgnoreCase)) - { - return "gzip"; - } - } - - return null; - } - - /// - /// Returns the optimized result for the IRequestContext. - /// Does not use or store results in any cache. - /// - /// - /// - /// - public object ToOptimizedResult(IRequest request, T dto) - { - return ToOptimizedResultInternal(request, dto); - } - - private object ToOptimizedResultInternal(IRequest request, T dto, IDictionary responseHeaders = null) - { - // TODO: @bond use Span and .Equals - var contentType = request.ResponseContentType?.Split(';')[0].Trim().ToLowerInvariant(); - - switch (contentType) - { - case "application/xml": - case "text/xml": - case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml - return GetHttpResult(request, SerializeToXmlString(dto), contentType, false, responseHeaders); - - case "application/json": - case "text/json": - return GetHttpResult(request, _jsonSerializer.SerializeToString(dto), contentType, false, responseHeaders); - default: - break; - } - - var isHeadRequest = string.Equals(request.Verb, "head", StringComparison.OrdinalIgnoreCase); - - var ms = new MemoryStream(); - var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType); - - writerFn(dto, ms); - - ms.Position = 0; - - if (isHeadRequest) - { - using (ms) - { - return GetHttpResult(request, Array.Empty(), contentType, true, responseHeaders); - } - } - - return GetHttpResult(request, ms, contentType, true, responseHeaders); - } - - private IHasHeaders GetCompressedResult( - byte[] content, - string requestedCompressionType, - IDictionary responseHeaders, - bool isHeadRequest, - string contentType) - { - if (responseHeaders == null) - { - responseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - content = Compress(content, requestedCompressionType); - responseHeaders[HeaderNames.ContentEncoding] = requestedCompressionType; - - responseHeaders[HeaderNames.Vary] = HeaderNames.AcceptEncoding; - - var contentLength = content.Length; - - if (isHeadRequest) - { - var result = new StreamWriter(Array.Empty(), contentType, contentLength); - AddResponseHeaders(result, responseHeaders); - return result; - } - else - { - var result = new StreamWriter(content, contentType, contentLength); - AddResponseHeaders(result, responseHeaders); - return result; - } - } - - private byte[] Compress(byte[] bytes, string compressionType) - { - if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase)) - { - return Deflate(bytes); - } - - if (string.Equals(compressionType, "gzip", StringComparison.OrdinalIgnoreCase)) - { - return GZip(bytes); - } - - throw new NotSupportedException(compressionType); - } - - private static byte[] Deflate(byte[] bytes) - { - // In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream - // Which means we must use MemoryStream since you have to use ToArray() on a closed Stream - using (var ms = new MemoryStream()) - using (var zipStream = new DeflateStream(ms, CompressionMode.Compress)) - { - zipStream.Write(bytes, 0, bytes.Length); - zipStream.Dispose(); - - return ms.ToArray(); - } - } - - private static byte[] GZip(byte[] buffer) - { - using (var ms = new MemoryStream()) - using (var zipStream = new GZipStream(ms, CompressionMode.Compress)) - { - zipStream.Write(buffer, 0, buffer.Length); - zipStream.Dispose(); - - return ms.ToArray(); - } - } - - private static string SerializeToXmlString(object from) - { - using (var ms = new MemoryStream()) - { - var xwSettings = new XmlWriterSettings(); - xwSettings.Encoding = new UTF8Encoding(false); - xwSettings.OmitXmlDeclaration = false; - - using (var xw = XmlWriter.Create(ms, xwSettings)) - { - var serializer = new DataContractSerializer(from.GetType()); - serializer.WriteObject(xw, from); - xw.Flush(); - ms.Seek(0, SeekOrigin.Begin); - using (var reader = new StreamReader(ms)) - { - return reader.ReadToEnd(); - } - } - } - } - - /// - /// Pres the process optimized result. - /// - private object GetCachedResult(IRequest requestContext, IDictionary responseHeaders, StaticResultOptions options) - { - bool noCache = requestContext.Headers[HeaderNames.CacheControl].ToString().IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1; - AddCachingHeaders(responseHeaders, options.CacheDuration, noCache, options.DateLastModified); - - if (!noCache) - { - if (!DateTime.TryParseExact(requestContext.Headers[HeaderNames.IfModifiedSince], HttpDateFormat, _enUSculture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var ifModifiedSinceHeader)) - { - _logger.LogDebug("Failed to parse If-Modified-Since header date: {0}", requestContext.Headers[HeaderNames.IfModifiedSince]); - return null; - } - - if (IsNotModified(ifModifiedSinceHeader, options.CacheDuration, options.DateLastModified)) - { - AddAgeHeader(responseHeaders, options.DateLastModified); - - var result = new HttpResult(Array.Empty(), options.ContentType ?? "text/html", HttpStatusCode.NotModified); - - AddResponseHeaders(result, responseHeaders); - - return result; - } - } - - return null; - } - - public Task GetStaticFileResult(IRequest requestContext, - string path, - FileShare fileShare = FileShare.Read) - { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - return GetStaticFileResult(requestContext, new StaticFileResultOptions - { - Path = path, - FileShare = fileShare - }); - } - - public Task GetStaticFileResult(IRequest requestContext, StaticFileResultOptions options) - { - var path = options.Path; - var fileShare = options.FileShare; - - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentException("Path can't be empty.", nameof(options)); - } - - if (fileShare != FileShare.Read && fileShare != FileShare.ReadWrite) - { - throw new ArgumentException("FileShare must be either Read or ReadWrite"); - } - - if (string.IsNullOrEmpty(options.ContentType)) - { - options.ContentType = MimeTypes.GetMimeType(path); - } - - if (!options.DateLastModified.HasValue) - { - options.DateLastModified = _fileSystem.GetLastWriteTimeUtc(path); - } - - options.ContentFactory = () => Task.FromResult(GetFileStream(path, fileShare)); - - options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary(StringComparer.OrdinalIgnoreCase); - - return GetStaticResult(requestContext, options); - } - - /// - /// Gets the file stream. - /// - /// The path. - /// The file share. - /// Stream. - private Stream GetFileStream(string path, FileShare fileShare) - { - return new FileStream(path, FileMode.Open, FileAccess.Read, fileShare); - } - - public Task GetStaticResult(IRequest requestContext, - Guid cacheKey, - DateTime? lastDateModified, - TimeSpan? cacheDuration, - string contentType, - Func> factoryFn, - IDictionary responseHeaders = null, - bool isHeadRequest = false) - { - return GetStaticResult(requestContext, new StaticResultOptions - { - CacheDuration = cacheDuration, - ContentFactory = factoryFn, - ContentType = contentType, - DateLastModified = lastDateModified, - IsHeadRequest = isHeadRequest, - ResponseHeaders = responseHeaders - }); - } - - public async Task GetStaticResult(IRequest requestContext, StaticResultOptions options) - { - options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary(StringComparer.OrdinalIgnoreCase); - - var contentType = options.ContentType; - if (!StringValues.IsNullOrEmpty(requestContext.Headers[HeaderNames.IfModifiedSince])) - { - // See if the result is already cached in the browser - var result = GetCachedResult(requestContext, options.ResponseHeaders, options); - - if (result != null) - { - return result; - } - } - - // TODO: We don't really need the option value - var isHeadRequest = options.IsHeadRequest || string.Equals(requestContext.Verb, "HEAD", StringComparison.OrdinalIgnoreCase); - var factoryFn = options.ContentFactory; - var responseHeaders = options.ResponseHeaders; - AddCachingHeaders(responseHeaders, options.CacheDuration, false, options.DateLastModified); - AddAgeHeader(responseHeaders, options.DateLastModified); - - var rangeHeader = requestContext.Headers[HeaderNames.Range]; - - if (!isHeadRequest && !string.IsNullOrEmpty(options.Path)) - { - var hasHeaders = new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem, _streamHelper) - { - OnComplete = options.OnComplete, - OnError = options.OnError, - FileShare = options.FileShare - }; - - AddResponseHeaders(hasHeaders, options.ResponseHeaders); - return hasHeaders; - } - - var stream = await factoryFn().ConfigureAwait(false); - - var totalContentLength = options.ContentLength; - if (!totalContentLength.HasValue) - { - try - { - totalContentLength = stream.Length; - } - catch (NotSupportedException) - { - } - } - - if (!string.IsNullOrWhiteSpace(rangeHeader) && totalContentLength.HasValue) - { - var hasHeaders = new RangeRequestWriter(rangeHeader, totalContentLength.Value, stream, contentType, isHeadRequest) - { - OnComplete = options.OnComplete - }; - - AddResponseHeaders(hasHeaders, options.ResponseHeaders); - return hasHeaders; - } - else - { - if (totalContentLength.HasValue) - { - responseHeaders["Content-Length"] = totalContentLength.Value.ToString(CultureInfo.InvariantCulture); - } - - if (isHeadRequest) - { - using (stream) - { - return GetHttpResult(requestContext, Array.Empty(), contentType, true, responseHeaders); - } - } - - var hasHeaders = new StreamWriter(stream, contentType) - { - OnComplete = options.OnComplete, - OnError = options.OnError - }; - - AddResponseHeaders(hasHeaders, options.ResponseHeaders); - return hasHeaders; - } - } - - /// - /// Adds the caching responseHeaders. - /// - private void AddCachingHeaders( - IDictionary responseHeaders, - TimeSpan? cacheDuration, - bool noCache, - DateTime? lastModifiedDate) - { - if (noCache) - { - responseHeaders[HeaderNames.CacheControl] = "no-cache, no-store, must-revalidate"; - responseHeaders[HeaderNames.Pragma] = "no-cache, no-store, must-revalidate"; - return; - } - - if (cacheDuration.HasValue) - { - responseHeaders[HeaderNames.CacheControl] = "public, max-age=" + cacheDuration.Value.TotalSeconds; - } - else - { - responseHeaders[HeaderNames.CacheControl] = "public"; - } - - if (lastModifiedDate.HasValue) - { - responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToUniversalTime().ToString(HttpDateFormat, _enUSculture); - } - } - - /// - /// Adds the age header. - /// - /// The responseHeaders. - /// The last date modified. - private static void AddAgeHeader(IDictionary responseHeaders, DateTime? lastDateModified) - { - if (lastDateModified.HasValue) - { - responseHeaders[HeaderNames.Age] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture); - } - } - - /// - /// Determines whether [is not modified] [the specified if modified since]. - /// - /// If modified since. - /// Duration of the cache. - /// The date modified. - /// true if [is not modified] [the specified if modified since]; otherwise, false. - private bool IsNotModified(DateTime ifModifiedSince, TimeSpan? cacheDuration, DateTime? dateModified) - { - if (dateModified.HasValue) - { - var lastModified = NormalizeDateForComparison(dateModified.Value); - ifModifiedSince = NormalizeDateForComparison(ifModifiedSince); - - return lastModified <= ifModifiedSince; - } - - if (cacheDuration.HasValue) - { - var cacheExpirationDate = ifModifiedSince.Add(cacheDuration.Value); - - if (DateTime.UtcNow < cacheExpirationDate) - { - return true; - } - } - - return false; - } - - - /// - /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that. - /// - /// The date. - /// DateTime. - private static DateTime NormalizeDateForComparison(DateTime date) - { - return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind); - } - - /// - /// Adds the response headers. - /// - /// The has options. - /// The response headers. - private static void AddResponseHeaders(IHasHeaders hasHeaders, IEnumerable> responseHeaders) - { - foreach (var item in responseHeaders) - { - hasHeaders.Headers[item.Key] = item.Value; - } - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs deleted file mode 100644 index 980c2cd3a..000000000 --- a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs +++ /dev/null @@ -1,212 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Buffers; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; -using Microsoft.Net.Http.Headers; - -namespace Emby.Server.Implementations.HttpServer -{ - public class RangeRequestWriter : IAsyncStreamWriter, IHttpResult - { - private const int BufferSize = 81920; - - private readonly Dictionary _options = new Dictionary(); - - private List> _requestedRanges; - - /// - /// Initializes a new instance of the class. - /// - /// The range header. - /// The content length. - /// The source. - /// Type of the content. - /// if set to true [is head request]. - public RangeRequestWriter(string rangeHeader, long contentLength, Stream source, string contentType, bool isHeadRequest) - { - if (string.IsNullOrEmpty(contentType)) - { - throw new ArgumentNullException(nameof(contentType)); - } - - RangeHeader = rangeHeader; - SourceStream = source; - IsHeadRequest = isHeadRequest; - - ContentType = contentType; - Headers[HeaderNames.ContentType] = contentType; - Headers[HeaderNames.AcceptRanges] = "bytes"; - StatusCode = HttpStatusCode.PartialContent; - - SetRangeValues(contentLength); - } - - /// - /// Gets or sets the source stream. - /// - /// The source stream. - private Stream SourceStream { get; set; } - private string RangeHeader { get; set; } - private bool IsHeadRequest { get; set; } - - private long RangeStart { get; set; } - private long RangeEnd { get; set; } - private long RangeLength { get; set; } - private long TotalContentLength { get; set; } - - public Action OnComplete { get; set; } - - /// - /// Additional HTTP Headers - /// - /// The headers. - public IDictionary Headers => _options; - - /// - /// Gets the requested ranges. - /// - /// The requested ranges. - protected List> RequestedRanges - { - get - { - if (_requestedRanges == null) - { - _requestedRanges = new List>(); - - // Example: bytes=0-,32-63 - var ranges = RangeHeader.Split('=')[1].Split(','); - - foreach (var range in ranges) - { - var vals = range.Split('-'); - - long start = 0; - long? end = null; - - if (!string.IsNullOrEmpty(vals[0])) - { - start = long.Parse(vals[0], CultureInfo.InvariantCulture); - } - - if (!string.IsNullOrEmpty(vals[1])) - { - end = long.Parse(vals[1], CultureInfo.InvariantCulture); - } - - _requestedRanges.Add(new KeyValuePair(start, end)); - } - } - - return _requestedRanges; - } - } - - public string ContentType { get; set; } - - public IRequest RequestContext { get; set; } - - public object Response { get; set; } - - public int Status { get; set; } - - public HttpStatusCode StatusCode - { - get => (HttpStatusCode)Status; - set => Status = (int)value; - } - - /// - /// Sets the range values. - /// - private void SetRangeValues(long contentLength) - { - var requestedRange = RequestedRanges[0]; - - TotalContentLength = contentLength; - - // If the requested range is "0-", we can optimize by just doing a stream copy - if (!requestedRange.Value.HasValue) - { - RangeEnd = TotalContentLength - 1; - } - else - { - RangeEnd = requestedRange.Value.Value; - } - - RangeStart = requestedRange.Key; - RangeLength = 1 + RangeEnd - RangeStart; - - Headers[HeaderNames.ContentLength] = RangeLength.ToString(CultureInfo.InvariantCulture); - Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; - - if (RangeStart > 0 && SourceStream.CanSeek) - { - SourceStream.Position = RangeStart; - } - } - - public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) - { - try - { - // Headers only - if (IsHeadRequest) - { - return; - } - - using (var source = SourceStream) - { - // If the requested range is "0-", we can optimize by just doing a stream copy - if (RangeEnd >= TotalContentLength - 1) - { - await source.CopyToAsync(responseStream, BufferSize, cancellationToken).ConfigureAwait(false); - } - else - { - await CopyToInternalAsync(source, responseStream, RangeLength, cancellationToken).ConfigureAwait(false); - } - } - } - finally - { - OnComplete?.Invoke(); - } - } - - private static async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) - { - var array = ArrayPool.Shared.Rent(BufferSize); - try - { - int bytesRead; - while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0) - { - var bytesToCopy = Math.Min(bytesRead, copyLength); - - await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy), cancellationToken).ConfigureAwait(false); - - copyLength -= bytesToCopy; - - if (copyLength <= 0) - { - break; - } - } - } - finally - { - ArrayPool.Shared.Return(array); - } - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs deleted file mode 100644 index a8cd2ac8f..000000000 --- a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Globalization; -using System.Text; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; - -namespace Emby.Server.Implementations.HttpServer -{ - /// - /// Class ResponseFilter. - /// - public class ResponseFilter - { - private readonly IHttpServer _server; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// The HTTP server. - /// The logger. - public ResponseFilter(IHttpServer server, ILogger logger) - { - _server = server; - _logger = logger; - } - - /// - /// Filters the response. - /// - /// The req. - /// The res. - /// The dto. - public void FilterResponse(IRequest req, HttpResponse res, object dto) - { - foreach(var (key, value) in _server.GetDefaultCorsHeaders(req)) - { - res.Headers.Add(key, value); - } - // Try to prevent compatibility view - res.Headers["Access-Control-Allow-Headers"] = "Accept, Accept-Language, Authorization, Cache-Control, " + - "Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, " + - "Content-Type, Cookie, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, " + - "Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, " + - "X-Emby-Authorization"; - - if (dto is Exception exception) - { - _logger.LogError(exception, "Error processing request for {RawUrl}", req.RawUrl); - - if (!string.IsNullOrEmpty(exception.Message)) - { - var error = exception.Message.Replace(Environment.NewLine, " ", StringComparison.Ordinal); - error = RemoveControlCharacters(error); - - res.Headers.Add("X-Application-Error-Code", error); - } - } - - if (dto is IHasHeaders hasHeaders) - { - if (!hasHeaders.Headers.ContainsKey(HeaderNames.Server)) - { - hasHeaders.Headers[HeaderNames.Server] = "Microsoft-NetCore/2.0, UPnP/1.0 DLNADOC/1.50"; - } - - // Content length has to be explicitly set on on HttpListenerResponse or it won't be happy - if (hasHeaders.Headers.TryGetValue(HeaderNames.ContentLength, out string contentLength) - && !string.IsNullOrEmpty(contentLength)) - { - var length = long.Parse(contentLength, CultureInfo.InvariantCulture); - - if (length > 0) - { - res.ContentLength = length; - } - } - } - } - - /// - /// Removes the control characters. - /// - /// The in string. - /// System.String. - public static string RemoveControlCharacters(string inString) - { - if (inString == null) - { - return null; - } - else if (inString.Length == 0) - { - return inString; - } - - var newString = new StringBuilder(inString.Length); - - foreach (var ch in inString) - { - if (!char.IsControl(ch)) - { - newString.Append(ch); - } - } - - return newString.ToString(); - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index 76c1d9bac..68d981ad1 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -1,17 +1,7 @@ #pragma warning disable CS1591 -using System; -using System.Linq; -using Emby.Server.Implementations.SocketSharp; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Security; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; namespace Emby.Server.Implementations.HttpServer.Security @@ -19,32 +9,11 @@ namespace Emby.Server.Implementations.HttpServer.Security public class AuthService : IAuthService { private readonly IAuthorizationContext _authorizationContext; - private readonly ISessionManager _sessionManager; - private readonly IServerConfigurationManager _config; - private readonly INetworkManager _networkManager; public AuthService( - IAuthorizationContext authorizationContext, - IServerConfigurationManager config, - ISessionManager sessionManager, - INetworkManager networkManager) + IAuthorizationContext authorizationContext) { _authorizationContext = authorizationContext; - _config = config; - _sessionManager = sessionManager; - _networkManager = networkManager; - } - - public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes) - { - ValidateUser(request, authAttributes); - } - - public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes) - { - var req = new WebSocketSharpRequest(request, null, request.Path); - var user = ValidateUser(req, authAttributes); - return user; } public AuthorizationInfo Authenticate(HttpRequest request) @@ -62,185 +31,5 @@ namespace Emby.Server.Implementations.HttpServer.Security return auth; } - - private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes) - { - // This code is executed before the service - var auth = _authorizationContext.GetAuthorizationInfo(request); - - if (!IsExemptFromAuthenticationToken(authAttributes, request)) - { - ValidateSecurityToken(request, auth.Token); - } - - if (authAttributes.AllowLocalOnly && !request.IsLocal) - { - throw new SecurityException("Operation not found."); - } - - var user = auth.User; - - if (user == null && auth.UserId != Guid.Empty) - { - throw new AuthenticationException("User with Id " + auth.UserId + " not found"); - } - - if (user != null) - { - ValidateUserAccess(user, request, authAttributes); - } - - var info = GetTokenInfo(request); - - if (!IsExemptFromRoles(auth, authAttributes, request, info)) - { - var roles = authAttributes.GetRoles(); - - ValidateRoles(roles, user); - } - - if (!string.IsNullOrEmpty(auth.DeviceId) && - !string.IsNullOrEmpty(auth.Client) && - !string.IsNullOrEmpty(auth.Device)) - { - _sessionManager.LogSessionActivity( - auth.Client, - auth.Version, - auth.DeviceId, - auth.Device, - request.RemoteIp, - user); - } - - return user; - } - - private void ValidateUserAccess( - User user, - IRequest request, - IAuthenticationAttributes authAttributes) - { - if (user.HasPermission(PermissionKind.IsDisabled)) - { - throw new SecurityException("User account has been disabled."); - } - - if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !_networkManager.IsInLocalNetwork(request.RemoteIp)) - { - throw new SecurityException("User account has been disabled."); - } - - if (!user.HasPermission(PermissionKind.IsAdministrator) - && !authAttributes.EscapeParentalControl - && !user.IsParentalScheduleAllowed()) - { - request.Response.Headers.Add("X-Application-Error-Code", "ParentalControl"); - - throw new SecurityException("This user account is not allowed access at this time."); - } - } - - private bool IsExemptFromAuthenticationToken(IAuthenticationAttributes authAttribtues, IRequest request) - { - if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard) - { - return true; - } - - if (authAttribtues.AllowLocal && request.IsLocal) - { - return true; - } - - if (authAttribtues.AllowLocalOnly && request.IsLocal) - { - return true; - } - - if (authAttribtues.IgnoreLegacyAuth) - { - return true; - } - - return false; - } - - private bool IsExemptFromRoles(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues, IRequest request, AuthenticationInfo tokenInfo) - { - if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard) - { - return true; - } - - if (authAttribtues.AllowLocal && request.IsLocal) - { - return true; - } - - if (authAttribtues.AllowLocalOnly && request.IsLocal) - { - return true; - } - - if (string.IsNullOrEmpty(auth.Token)) - { - return true; - } - - if (tokenInfo != null && tokenInfo.UserId.Equals(Guid.Empty)) - { - return true; - } - - return false; - } - - private static void ValidateRoles(string[] roles, User user) - { - if (roles.Contains("admin", StringComparer.OrdinalIgnoreCase)) - { - if (user == null || !user.HasPermission(PermissionKind.IsAdministrator)) - { - throw new SecurityException("User does not have admin access."); - } - } - - if (roles.Contains("delete", StringComparer.OrdinalIgnoreCase)) - { - if (user == null || !user.HasPermission(PermissionKind.EnableContentDeletion)) - { - throw new SecurityException("User does not have delete access."); - } - } - - if (roles.Contains("download", StringComparer.OrdinalIgnoreCase)) - { - if (user == null || !user.HasPermission(PermissionKind.EnableContentDownloading)) - { - throw new SecurityException("User does not have download access."); - } - } - } - - private static AuthenticationInfo GetTokenInfo(IRequest request) - { - request.Items.TryGetValue("OriginalAuthenticationInfo", out var info); - return info as AuthenticationInfo; - } - - private void ValidateSecurityToken(IRequest request, string token) - { - if (string.IsNullOrEmpty(token)) - { - throw new AuthenticationException("Access token is required."); - } - - var info = GetTokenInfo(request); - - if (info == null) - { - throw new AuthenticationException("Access token is invalid or expired."); - } - } } } diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs index fb93fae3e..eec8ac486 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -7,7 +7,6 @@ using System.Net; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; -using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; @@ -26,12 +25,12 @@ namespace Emby.Server.Implementations.HttpServer.Security public AuthorizationInfo GetAuthorizationInfo(object requestContext) { - return GetAuthorizationInfo((IRequest)requestContext); + return GetAuthorizationInfo((HttpContext)requestContext); } - public AuthorizationInfo GetAuthorizationInfo(IRequest requestContext) + public AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext) { - if (requestContext.Items.TryGetValue("AuthorizationInfo", out var cached)) + if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached)) { return (AuthorizationInfo)cached; } @@ -52,18 +51,18 @@ namespace Emby.Server.Implementations.HttpServer.Security /// /// The HTTP req. /// Dictionary{System.StringSystem.String}. - private AuthorizationInfo GetAuthorization(IRequest httpReq) + private AuthorizationInfo GetAuthorization(HttpContext httpReq) { var auth = GetAuthorizationDictionary(httpReq); var (authInfo, originalAuthInfo) = - GetAuthorizationInfoFromDictionary(auth, httpReq.Headers, httpReq.QueryString); + GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query); if (originalAuthInfo != null) { - httpReq.Items["OriginalAuthenticationInfo"] = originalAuthInfo; + httpReq.Request.HttpContext.Items["OriginalAuthenticationInfo"] = originalAuthInfo; } - httpReq.Items["AuthorizationInfo"] = authInfo; + httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo; return authInfo; } @@ -203,13 +202,13 @@ namespace Emby.Server.Implementations.HttpServer.Security /// /// The HTTP req. /// Dictionary{System.StringSystem.String}. - private Dictionary GetAuthorizationDictionary(IRequest httpReq) + private Dictionary GetAuthorizationDictionary(HttpContext httpReq) { - var auth = httpReq.Headers["X-Emby-Authorization"]; + var auth = httpReq.Request.Headers["X-Emby-Authorization"]; if (string.IsNullOrEmpty(auth)) { - auth = httpReq.Headers[HeaderNames.Authorization]; + auth = httpReq.Request.Headers[HeaderNames.Authorization]; } return GetAuthorization(auth); diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs index 03fcfa53d..8777c59b7 100644 --- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs @@ -2,11 +2,11 @@ using System; using Jellyfin.Data.Entities; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; namespace Emby.Server.Implementations.HttpServer.Security { @@ -23,26 +23,20 @@ namespace Emby.Server.Implementations.HttpServer.Security _sessionManager = sessionManager; } - public SessionInfo GetSession(IRequest requestContext) + public SessionInfo GetSession(HttpContext requestContext) { var authorization = _authContext.GetAuthorizationInfo(requestContext); var user = authorization.User; - return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.RemoteIp, user); - } - - private AuthenticationInfo GetTokenInfo(IRequest request) - { - request.Items.TryGetValue("OriginalAuthenticationInfo", out var info); - return info as AuthenticationInfo; + return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.Request.RemoteIp(), user); } public SessionInfo GetSession(object requestContext) { - return GetSession((IRequest)requestContext); + return GetSession((HttpContext)requestContext); } - public User GetUser(IRequest requestContext) + public User GetUser(HttpContext requestContext) { var session = GetSession(requestContext); @@ -51,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security public User GetUser(object requestContext) { - return GetUser((IRequest)requestContext); + return GetUser((HttpContext)requestContext); } } } diff --git a/Emby.Server.Implementations/HttpServer/StreamWriter.cs b/Emby.Server.Implementations/HttpServer/StreamWriter.cs deleted file mode 100644 index 00e3ab8fe..000000000 --- a/Emby.Server.Implementations/HttpServer/StreamWriter.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; -using Microsoft.Net.Http.Headers; - -namespace Emby.Server.Implementations.HttpServer -{ - /// - /// Class StreamWriter. - /// - public class StreamWriter : IAsyncStreamWriter, IHasHeaders - { - /// - /// The options. - /// - private readonly IDictionary _options = new Dictionary(); - - /// - /// Initializes a new instance of the class. - /// - /// The source. - /// Type of the content. - public StreamWriter(Stream source, string contentType) - { - if (string.IsNullOrEmpty(contentType)) - { - throw new ArgumentNullException(nameof(contentType)); - } - - SourceStream = source; - - Headers["Content-Type"] = contentType; - - if (source.CanSeek) - { - Headers[HeaderNames.ContentLength] = source.Length.ToString(CultureInfo.InvariantCulture); - } - - Headers[HeaderNames.ContentType] = contentType; - } - - /// - /// Initializes a new instance of the class. - /// - /// The source. - /// Type of the content. - /// The content length. - public StreamWriter(byte[] source, string contentType, int contentLength) - { - if (string.IsNullOrEmpty(contentType)) - { - throw new ArgumentNullException(nameof(contentType)); - } - - SourceBytes = source; - - Headers[HeaderNames.ContentLength] = contentLength.ToString(CultureInfo.InvariantCulture); - Headers[HeaderNames.ContentType] = contentType; - } - - /// - /// Gets or sets the source stream. - /// - /// The source stream. - private Stream SourceStream { get; set; } - - private byte[] SourceBytes { get; set; } - - /// - /// Gets the options. - /// - /// The options. - public IDictionary Headers => _options; - - /// - /// Fires when complete. - /// - public Action OnComplete { get; set; } - - /// - /// Fires when an error occours. - /// - public Action OnError { get; set; } - - /// - public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) - { - try - { - var bytes = SourceBytes; - - if (bytes != null) - { - await responseStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false); - } - else - { - using (var src = SourceStream) - { - await src.CopyToAsync(responseStream, cancellationToken).ConfigureAwait(false); - } - } - } - catch - { - OnError?.Invoke(); - - throw; - } - finally - { - OnComplete?.Invoke(); - } - } - } -} diff --git a/Emby.Server.Implementations/Services/HttpResult.cs b/Emby.Server.Implementations/Services/HttpResult.cs deleted file mode 100644 index 8ba86f756..000000000 --- a/Emby.Server.Implementations/Services/HttpResult.cs +++ /dev/null @@ -1,64 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; - -namespace Emby.Server.Implementations.Services -{ - public class HttpResult - : IHttpResult, IAsyncStreamWriter - { - public HttpResult(object response, string contentType, HttpStatusCode statusCode) - { - this.Headers = new Dictionary(); - - this.Response = response; - this.ContentType = contentType; - this.StatusCode = statusCode; - } - - public object Response { get; set; } - - public string ContentType { get; set; } - - public IDictionary Headers { get; private set; } - - public int Status { get; set; } - - public HttpStatusCode StatusCode - { - get => (HttpStatusCode)Status; - set => Status = (int)value; - } - - public IRequest RequestContext { get; set; } - - public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) - { - var response = RequestContext?.Response; - - if (this.Response is byte[] bytesResponse) - { - var contentLength = bytesResponse.Length; - - if (response != null) - { - response.ContentLength = contentLength; - } - - if (contentLength > 0) - { - await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false); - } - - return; - } - - await ResponseHelper.WriteObject(this.RequestContext, this.Response, response).ConfigureAwait(false); - } - } -} diff --git a/Emby.Server.Implementations/Services/RequestHelper.cs b/Emby.Server.Implementations/Services/RequestHelper.cs deleted file mode 100644 index 1f9c7fc22..000000000 --- a/Emby.Server.Implementations/Services/RequestHelper.cs +++ /dev/null @@ -1,51 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.IO; -using System.Threading.Tasks; -using Emby.Server.Implementations.HttpServer; - -namespace Emby.Server.Implementations.Services -{ - public class RequestHelper - { - public static Func> GetRequestReader(HttpListenerHost host, string contentType) - { - switch (GetContentTypeWithoutEncoding(contentType)) - { - case "application/xml": - case "text/xml": - case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml - return host.DeserializeXml; - - case "application/json": - case "text/json": - return host.DeserializeJson; - } - - return null; - } - - public static Action GetResponseWriter(HttpListenerHost host, string contentType) - { - switch (GetContentTypeWithoutEncoding(contentType)) - { - case "application/xml": - case "text/xml": - case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml - return host.SerializeToXml; - - case "application/json": - case "text/json": - return host.SerializeToJson; - } - - return null; - } - - private static string GetContentTypeWithoutEncoding(string contentType) - { - return contentType?.Split(';')[0].ToLowerInvariant().Trim(); - } - } -} diff --git a/Emby.Server.Implementations/Services/ResponseHelper.cs b/Emby.Server.Implementations/Services/ResponseHelper.cs deleted file mode 100644 index a329b531d..000000000 --- a/Emby.Server.Implementations/Services/ResponseHelper.cs +++ /dev/null @@ -1,141 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Globalization; -using System.IO; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.HttpServer; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; - -namespace Emby.Server.Implementations.Services -{ - public static class ResponseHelper - { - public static Task WriteToResponse(HttpResponse response, IRequest request, object result, CancellationToken cancellationToken) - { - if (result == null) - { - if (response.StatusCode == (int)HttpStatusCode.OK) - { - response.StatusCode = (int)HttpStatusCode.NoContent; - } - - response.ContentLength = 0; - return Task.CompletedTask; - } - - var httpResult = result as IHttpResult; - if (httpResult != null) - { - httpResult.RequestContext = request; - request.ResponseContentType = httpResult.ContentType ?? request.ResponseContentType; - } - - var defaultContentType = request.ResponseContentType; - - if (httpResult != null) - { - if (httpResult.RequestContext == null) - { - httpResult.RequestContext = request; - } - - response.StatusCode = httpResult.Status; - } - - if (result is IHasHeaders responseOptions) - { - foreach (var responseHeaders in responseOptions.Headers) - { - if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase)) - { - response.ContentLength = long.Parse(responseHeaders.Value, CultureInfo.InvariantCulture); - continue; - } - - response.Headers.Add(responseHeaders.Key, responseHeaders.Value); - } - } - - // ContentType='text/html' is the default for a HttpResponse - // Do not override if another has been set - if (response.ContentType == null || response.ContentType == "text/html") - { - response.ContentType = defaultContentType; - } - - if (response.ContentType == "application/json") - { - response.ContentType += "; charset=utf-8"; - } - - switch (result) - { - case IAsyncStreamWriter asyncStreamWriter: - return asyncStreamWriter.WriteToAsync(response.Body, cancellationToken); - case IStreamWriter streamWriter: - streamWriter.WriteTo(response.Body); - return Task.CompletedTask; - case FileWriter fileWriter: - return fileWriter.WriteToAsync(response, cancellationToken); - case Stream stream: - return CopyStream(stream, response.Body); - case byte[] bytes: - response.ContentType = "application/octet-stream"; - response.ContentLength = bytes.Length; - - if (bytes.Length > 0) - { - return response.Body.WriteAsync(bytes, 0, bytes.Length, cancellationToken); - } - - return Task.CompletedTask; - case string responseText: - var responseTextAsBytes = Encoding.UTF8.GetBytes(responseText); - response.ContentLength = responseTextAsBytes.Length; - - if (responseTextAsBytes.Length > 0) - { - return response.Body.WriteAsync(responseTextAsBytes, 0, responseTextAsBytes.Length, cancellationToken); - } - - return Task.CompletedTask; - } - - return WriteObject(request, result, response); - } - - private static async Task CopyStream(Stream src, Stream dest) - { - using (src) - { - await src.CopyToAsync(dest).ConfigureAwait(false); - } - } - - public static async Task WriteObject(IRequest request, object result, HttpResponse response) - { - var contentType = request.ResponseContentType; - var serializer = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType); - - using (var ms = new MemoryStream()) - { - serializer(result, ms); - - ms.Position = 0; - - var contentLength = ms.Length; - response.ContentLength = contentLength; - - if (contentLength > 0) - { - await ms.CopyToAsync(response.Body).ConfigureAwait(false); - } - } - } - } -} diff --git a/Emby.Server.Implementations/Services/ServiceController.cs b/Emby.Server.Implementations/Services/ServiceController.cs deleted file mode 100644 index 47e7261e8..000000000 --- a/Emby.Server.Implementations/Services/ServiceController.cs +++ /dev/null @@ -1,202 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Threading.Tasks; -using Emby.Server.Implementations.HttpServer; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Services -{ - public delegate object ActionInvokerFn(object intance, object request); - - public delegate void VoidActionInvokerFn(object intance, object request); - - public class ServiceController - { - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - public ServiceController(ILogger logger) - { - _logger = logger; - } - - public void Init(HttpListenerHost appHost, IEnumerable serviceTypes) - { - foreach (var serviceType in serviceTypes) - { - RegisterService(appHost, serviceType); - } - } - - public void RegisterService(HttpListenerHost appHost, Type serviceType) - { - // Make sure the provided type implements IService - if (!typeof(IService).IsAssignableFrom(serviceType)) - { - _logger.LogWarning("Tried to register a service that does not implement IService: {ServiceType}", serviceType); - return; - } - - var processedReqs = new HashSet(); - - var actions = ServiceExecGeneral.Reset(serviceType); - - foreach (var mi in serviceType.GetActions()) - { - var requestType = mi.GetParameters()[0].ParameterType; - if (processedReqs.Contains(requestType)) - { - continue; - } - - processedReqs.Add(requestType); - - ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions); - - // var returnMarker = GetTypeWithGenericTypeDefinitionOf(requestType, typeof(IReturn<>)); - // var responseType = returnMarker != null ? - // GetGenericArguments(returnMarker)[0] - // : mi.ReturnType != typeof(object) && mi.ReturnType != typeof(void) ? - // mi.ReturnType - // : Type.GetType(requestType.FullName + "Response"); - - RegisterRestPaths(appHost, requestType, serviceType); - - appHost.AddServiceInfo(serviceType, requestType); - } - } - - public readonly RestPath.RestPathMap RestPathMap = new RestPath.RestPathMap(); - - public void RegisterRestPaths(HttpListenerHost appHost, Type requestType, Type serviceType) - { - var attrs = appHost.GetRouteAttributes(requestType); - foreach (var attr in attrs) - { - var restPath = new RestPath(appHost.CreateInstance, appHost.GetParseFn, requestType, serviceType, attr.Path, attr.Verbs, attr.IsHidden, attr.Summary, attr.Description); - - RegisterRestPath(restPath); - } - } - - private static readonly char[] InvalidRouteChars = new[] { '?', '&' }; - - public void RegisterRestPath(RestPath restPath) - { - if (restPath.Path[0] != '/') - { - throw new ArgumentException( - string.Format( - CultureInfo.InvariantCulture, - "Route '{0}' on '{1}' must start with a '/'", - restPath.Path, - restPath.RequestType.GetMethodName())); - } - - if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1) - { - throw new ArgumentException( - string.Format( - CultureInfo.InvariantCulture, - "Route '{0}' on '{1}' contains invalid chars. ", - restPath.Path, - restPath.RequestType.GetMethodName())); - } - - if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List pathsAtFirstMatch)) - { - pathsAtFirstMatch.Add(restPath); - } - else - { - RestPathMap[restPath.FirstMatchHashKey] = new List() { restPath }; - } - } - - public RestPath GetRestPathForRequest(string httpMethod, string pathInfo) - { - var matchUsingPathParts = RestPath.GetPathPartsForMatching(pathInfo); - - List firstMatches; - - var yieldedHashMatches = RestPath.GetFirstMatchHashKeys(matchUsingPathParts); - foreach (var potentialHashMatch in yieldedHashMatches) - { - if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches)) - { - continue; - } - - var bestScore = -1; - RestPath bestMatch = null; - foreach (var restPath in firstMatches) - { - var score = restPath.MatchScore(httpMethod, matchUsingPathParts); - if (score > bestScore) - { - bestScore = score; - bestMatch = restPath; - } - } - - if (bestScore > 0 && bestMatch != null) - { - return bestMatch; - } - } - - var yieldedWildcardMatches = RestPath.GetFirstMatchWildCardHashKeys(matchUsingPathParts); - foreach (var potentialHashMatch in yieldedWildcardMatches) - { - if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches)) - { - continue; - } - - var bestScore = -1; - RestPath bestMatch = null; - foreach (var restPath in firstMatches) - { - var score = restPath.MatchScore(httpMethod, matchUsingPathParts); - if (score > bestScore) - { - bestScore = score; - bestMatch = restPath; - } - } - - if (bestScore > 0 && bestMatch != null) - { - return bestMatch; - } - } - - return null; - } - - public Task Execute(HttpListenerHost httpHost, object requestDto, IRequest req) - { - var requestType = requestDto.GetType(); - req.OperationName = requestType.Name; - - var serviceType = httpHost.GetServiceTypeByRequest(requestType); - - var service = httpHost.CreateInstance(serviceType); - - if (service is IRequiresRequest serviceRequiresContext) - { - serviceRequiresContext.Request = req; - } - - // Executes the service and returns the result - return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName()); - } - } -} diff --git a/Emby.Server.Implementations/Services/ServiceExec.cs b/Emby.Server.Implementations/Services/ServiceExec.cs deleted file mode 100644 index 7b970627e..000000000 --- a/Emby.Server.Implementations/Services/ServiceExec.cs +++ /dev/null @@ -1,230 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; - -namespace Emby.Server.Implementations.Services -{ - public static class ServiceExecExtensions - { - public static string[] AllVerbs = new[] { - "OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", // RFC 2616 - "PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", // RFC 2518 - "VERSION-CONTROL", "REPORT", "CHECKOUT", "CHECKIN", "UNCHECKOUT", - "MKWORKSPACE", "UPDATE", "LABEL", "MERGE", "BASELINE-CONTROL", "MKACTIVITY", // RFC 3253 - "ORDERPATCH", // RFC 3648 - "ACL", // RFC 3744 - "PATCH", // https://datatracker.ietf.org/doc/draft-dusseault-http-patch/ - "SEARCH", // https://datatracker.ietf.org/doc/draft-reschke-webdav-search/ - "BCOPY", "BDELETE", "BMOVE", "BPROPFIND", "BPROPPATCH", "NOTIFY", - "POLL", "SUBSCRIBE", "UNSUBSCRIBE" - }; - - public static List GetActions(this Type serviceType) - { - var list = new List(); - - foreach (var mi in serviceType.GetRuntimeMethods()) - { - if (!mi.IsPublic) - { - continue; - } - - if (mi.IsStatic) - { - continue; - } - - if (mi.GetParameters().Length != 1) - { - continue; - } - - var actionName = mi.Name; - if (!AllVerbs.Contains(actionName, StringComparer.OrdinalIgnoreCase)) - { - continue; - } - - list.Add(mi); - } - - return list; - } - } - - internal static class ServiceExecGeneral - { - private static Dictionary execMap = new Dictionary(); - - public static void CreateServiceRunnersFor(Type requestType, List actions) - { - foreach (var actionCtx in actions) - { - if (execMap.ContainsKey(actionCtx.Id)) - { - continue; - } - - execMap[actionCtx.Id] = actionCtx; - } - } - - public static Task Execute(Type serviceType, IRequest request, object instance, object requestDto, string requestName) - { - var actionName = request.Verb ?? "POST"; - - if (execMap.TryGetValue(ServiceMethod.Key(serviceType, actionName, requestName), out ServiceMethod actionContext)) - { - if (actionContext.RequestFilters != null) - { - foreach (var requestFilter in actionContext.RequestFilters) - { - requestFilter.RequestFilter(request, request.Response, requestDto); - if (request.Response.HasStarted) - { - Task.FromResult(null); - } - } - } - - var response = actionContext.ServiceAction(instance, requestDto); - - if (response is Task taskResponse) - { - return GetTaskResult(taskResponse); - } - - return Task.FromResult(response); - } - - var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLowerInvariant(); - throw new NotImplementedException( - string.Format( - CultureInfo.InvariantCulture, - "Could not find method named {1}({0}) or Any({0}) on Service {2}", - requestDto.GetType().GetMethodName(), - expectedMethodName, - serviceType.GetMethodName())); - } - - private static async Task GetTaskResult(Task task) - { - try - { - if (task is Task taskObject) - { - return await taskObject.ConfigureAwait(false); - } - - await task.ConfigureAwait(false); - - var type = task.GetType().GetTypeInfo(); - if (!type.IsGenericType) - { - return null; - } - - var resultProperty = type.GetDeclaredProperty("Result"); - if (resultProperty == null) - { - return null; - } - - var result = resultProperty.GetValue(task); - - // hack alert - if (result.GetType().Name.IndexOf("voidtaskresult", StringComparison.OrdinalIgnoreCase) != -1) - { - return null; - } - - return result; - } - catch (TypeAccessException) - { - return null; // return null for void Task's - } - } - - public static List Reset(Type serviceType) - { - var actions = new List(); - - foreach (var mi in serviceType.GetActions()) - { - var actionName = mi.Name; - var args = mi.GetParameters(); - - var requestType = args[0].ParameterType; - var actionCtx = new ServiceMethod - { - Id = ServiceMethod.Key(serviceType, actionName, requestType.GetMethodName()) - }; - - actionCtx.ServiceAction = CreateExecFn(serviceType, requestType, mi); - - var reqFilters = new List(); - - foreach (var attr in mi.GetCustomAttributes(true)) - { - if (attr is IHasRequestFilter hasReqFilter) - { - reqFilters.Add(hasReqFilter); - } - } - - if (reqFilters.Count > 0) - { - actionCtx.RequestFilters = reqFilters.OrderBy(i => i.Priority).ToArray(); - } - - actions.Add(actionCtx); - } - - return actions; - } - - private static ActionInvokerFn CreateExecFn(Type serviceType, Type requestType, MethodInfo mi) - { - var serviceParam = Expression.Parameter(typeof(object), "serviceObj"); - var serviceStrong = Expression.Convert(serviceParam, serviceType); - - var requestDtoParam = Expression.Parameter(typeof(object), "requestDto"); - var requestDtoStrong = Expression.Convert(requestDtoParam, requestType); - - Expression callExecute = Expression.Call( - serviceStrong, mi, requestDtoStrong); - - if (mi.ReturnType != typeof(void)) - { - var executeFunc = Expression.Lambda( - callExecute, - serviceParam, - requestDtoParam).Compile(); - - return executeFunc; - } - else - { - var executeFunc = Expression.Lambda( - callExecute, - serviceParam, - requestDtoParam).Compile(); - - return (service, request) => - { - executeFunc(service, request); - return null; - }; - } - } - } -} diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs deleted file mode 100644 index b4166f771..000000000 --- a/Emby.Server.Implementations/Services/ServiceHandler.cs +++ /dev/null @@ -1,212 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Net.Mime; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.HttpServer; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Services -{ - public class ServiceHandler - { - private RestPath _restPath; - - private string _responseContentType; - - internal ServiceHandler(RestPath restPath, string responseContentType) - { - _restPath = restPath; - _responseContentType = responseContentType; - } - - protected static Task CreateContentTypeRequest(HttpListenerHost host, IRequest httpReq, Type requestType, string contentType) - { - if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0) - { - var deserializer = RequestHelper.GetRequestReader(host, contentType); - if (deserializer != null) - { - return deserializer.Invoke(requestType, httpReq.InputStream); - } - } - - return Task.FromResult(host.CreateInstance(requestType)); - } - - public static string GetSanitizedPathInfo(string pathInfo, out string contentType) - { - contentType = null; - var pos = pathInfo.LastIndexOf('.'); - if (pos != -1) - { - var format = pathInfo.AsSpan().Slice(pos + 1); - contentType = GetFormatContentType(format); - if (contentType != null) - { - pathInfo = pathInfo.Substring(0, pos); - } - } - - return pathInfo; - } - - private static string GetFormatContentType(ReadOnlySpan format) - { - if (format.Equals("json", StringComparison.Ordinal)) - { - return MediaTypeNames.Application.Json; - } - else if (format.Equals("xml", StringComparison.Ordinal)) - { - return MediaTypeNames.Application.Xml; - } - - return null; - } - - public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, CancellationToken cancellationToken) - { - httpReq.Items["__route"] = _restPath; - - if (_responseContentType != null) - { - httpReq.ResponseContentType = _responseContentType; - } - - var request = await CreateRequest(httpHost, httpReq, _restPath).ConfigureAwait(false); - - httpHost.ApplyRequestFilters(httpReq, httpRes, request); - - httpRes.HttpContext.SetServiceStackRequest(httpReq); - var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false); - - // Apply response filters - foreach (var responseFilter in httpHost.ResponseFilters) - { - responseFilter(httpReq, httpRes, response); - } - - await ResponseHelper.WriteToResponse(httpRes, httpReq, response, cancellationToken).ConfigureAwait(false); - } - - public static async Task CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath) - { - var requestType = restPath.RequestType; - - if (RequireqRequestStream(requestType)) - { - // Used by IRequiresRequestStream - var requestParams = GetRequestParams(httpReq.Response.HttpContext.Request); - var request = ServiceHandler.CreateRequest(httpReq, restPath, requestParams, host.CreateInstance(requestType)); - - var rawReq = (IRequiresRequestStream)request; - rawReq.RequestStream = httpReq.InputStream; - return rawReq; - } - else - { - var requestParams = GetFlattenedRequestParams(httpReq.Response.HttpContext.Request); - - var requestDto = await CreateContentTypeRequest(host, httpReq, restPath.RequestType, httpReq.ContentType).ConfigureAwait(false); - - return CreateRequest(httpReq, restPath, requestParams, requestDto); - } - } - - public static bool RequireqRequestStream(Type requestType) - { - var requiresRequestStreamTypeInfo = typeof(IRequiresRequestStream).GetTypeInfo(); - - return requiresRequestStreamTypeInfo.IsAssignableFrom(requestType.GetTypeInfo()); - } - - public static object CreateRequest(IRequest httpReq, RestPath restPath, Dictionary requestParams, object requestDto) - { - var pathInfo = !restPath.IsWildCardPath - ? GetSanitizedPathInfo(httpReq.PathInfo, out _) - : httpReq.PathInfo; - - return restPath.CreateRequest(pathInfo, requestParams, requestDto); - } - - /// - /// Duplicate Params are given a unique key by appending a #1 suffix - /// - private static Dictionary GetRequestParams(HttpRequest request) - { - var map = new Dictionary(); - - foreach (var pair in request.Query) - { - var values = pair.Value; - if (values.Count == 1) - { - map[pair.Key] = values[0]; - } - else - { - for (var i = 0; i < values.Count; i++) - { - map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i]; - } - } - } - - if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT")) - && request.HasFormContentType) - { - foreach (var pair in request.Form) - { - var values = pair.Value; - if (values.Count == 1) - { - map[pair.Key] = values[0]; - } - else - { - for (var i = 0; i < values.Count; i++) - { - map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i]; - } - } - } - } - - return map; - } - - private static bool IsMethod(string method, string expected) - => string.Equals(method, expected, StringComparison.OrdinalIgnoreCase); - - /// - /// Duplicate params have their values joined together in a comma-delimited string. - /// - private static Dictionary GetFlattenedRequestParams(HttpRequest request) - { - var map = new Dictionary(); - - foreach (var pair in request.Query) - { - map[pair.Key] = pair.Value; - } - - if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT")) - && request.HasFormContentType) - { - foreach (var pair in request.Form) - { - map[pair.Key] = pair.Value; - } - } - - return map; - } - } -} diff --git a/Emby.Server.Implementations/Services/ServiceMethod.cs b/Emby.Server.Implementations/Services/ServiceMethod.cs deleted file mode 100644 index 5116cc04f..000000000 --- a/Emby.Server.Implementations/Services/ServiceMethod.cs +++ /dev/null @@ -1,20 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace Emby.Server.Implementations.Services -{ - public class ServiceMethod - { - public string Id { get; set; } - - public ActionInvokerFn ServiceAction { get; set; } - - public MediaBrowser.Model.Services.IHasRequestFilter[] RequestFilters { get; set; } - - public static string Key(Type serviceType, string method, string requestDtoName) - { - return serviceType.FullName + " " + method.ToUpperInvariant() + " " + requestDtoName; - } - } -} diff --git a/Emby.Server.Implementations/Services/ServicePath.cs b/Emby.Server.Implementations/Services/ServicePath.cs deleted file mode 100644 index 0d4728b43..000000000 --- a/Emby.Server.Implementations/Services/ServicePath.cs +++ /dev/null @@ -1,550 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.Services -{ - public class RestPath - { - private const string WildCard = "*"; - private const char WildCardChar = '*'; - private const string PathSeperator = "/"; - private const char PathSeperatorChar = '/'; - private const char ComponentSeperator = '.'; - private const string VariablePrefix = "{"; - - private readonly bool[] componentsWithSeparators; - - private readonly string restPath; - public bool IsWildCardPath { get; private set; } - - private readonly string[] literalsToMatch; - - private readonly string[] variablesNames; - - private readonly bool[] isWildcard; - private readonly int wildcardCount = 0; - - internal static string[] IgnoreAttributesNamed = new[] - { - nameof(JsonIgnoreAttribute) - }; - - private static Type _excludeType = typeof(Stream); - - public int VariableArgsCount { get; set; } - - /// - /// The number of segments separated by '/' determinable by path.Split('/').Length - /// e.g. /path/to/here.ext == 3 - /// - public int PathComponentsCount { get; set; } - - /// - /// Gets or sets the total number of segments after subparts have been exploded ('.') - /// e.g. /path/to/here.ext == 4. - /// - public int TotalComponentsCount { get; set; } - - public string[] Verbs { get; private set; } - - public Type RequestType { get; private set; } - - public Type ServiceType { get; private set; } - - public string Path => this.restPath; - - public string Summary { get; private set; } - - public string Description { get; private set; } - - public bool IsHidden { get; private set; } - - public static string[] GetPathPartsForMatching(string pathInfo) - { - return pathInfo.ToLowerInvariant().Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries); - } - - public static List GetFirstMatchHashKeys(string[] pathPartsForMatching) - { - var hashPrefix = pathPartsForMatching.Length + PathSeperator; - return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching); - } - - public static List GetFirstMatchWildCardHashKeys(string[] pathPartsForMatching) - { - const string HashPrefix = WildCard + PathSeperator; - return GetPotentialMatchesWithPrefix(HashPrefix, pathPartsForMatching); - } - - private static List GetPotentialMatchesWithPrefix(string hashPrefix, string[] pathPartsForMatching) - { - var list = new List(); - - foreach (var part in pathPartsForMatching) - { - list.Add(hashPrefix + part); - - if (part.IndexOf(ComponentSeperator, StringComparison.Ordinal) == -1) - { - continue; - } - - var subParts = part.Split(ComponentSeperator); - foreach (var subPart in subParts) - { - list.Add(hashPrefix + subPart); - } - } - - return list; - } - - public RestPath(Func createInstanceFn, Func> getParseFn, Type requestType, Type serviceType, string path, string verbs, bool isHidden = false, string summary = null, string description = null) - { - this.RequestType = requestType; - this.ServiceType = serviceType; - this.Summary = summary; - this.IsHidden = isHidden; - this.Description = description; - this.restPath = path; - - this.Verbs = string.IsNullOrWhiteSpace(verbs) ? ServiceExecExtensions.AllVerbs : verbs.ToUpperInvariant().Split(new[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries); - - var componentsList = new List(); - - // We only split on '.' if the restPath has them. Allows for /{action}.{type} - var hasSeparators = new List(); - foreach (var component in this.restPath.Split(PathSeperatorChar)) - { - if (string.IsNullOrEmpty(component)) - { - continue; - } - - if (component.IndexOf(VariablePrefix, StringComparison.OrdinalIgnoreCase) != -1 - && component.IndexOf(ComponentSeperator, StringComparison.Ordinal) != -1) - { - hasSeparators.Add(true); - componentsList.AddRange(component.Split(ComponentSeperator)); - } - else - { - hasSeparators.Add(false); - componentsList.Add(component); - } - } - - var components = componentsList.ToArray(); - this.TotalComponentsCount = components.Length; - - this.literalsToMatch = new string[this.TotalComponentsCount]; - this.variablesNames = new string[this.TotalComponentsCount]; - this.isWildcard = new bool[this.TotalComponentsCount]; - this.componentsWithSeparators = hasSeparators.ToArray(); - this.PathComponentsCount = this.componentsWithSeparators.Length; - string firstLiteralMatch = null; - - for (var i = 0; i < components.Length; i++) - { - var component = components[i]; - - if (component.StartsWith(VariablePrefix, StringComparison.Ordinal)) - { - var variableName = component.Substring(1, component.Length - 2); - if (variableName[variableName.Length - 1] == WildCardChar) - { - this.isWildcard[i] = true; - variableName = variableName.Substring(0, variableName.Length - 1); - } - - this.variablesNames[i] = variableName; - this.VariableArgsCount++; - } - else - { - this.literalsToMatch[i] = component.ToLowerInvariant(); - - if (firstLiteralMatch == null) - { - firstLiteralMatch = this.literalsToMatch[i]; - } - } - } - - for (var i = 0; i < components.Length - 1; i++) - { - if (!this.isWildcard[i]) - { - continue; - } - - if (this.literalsToMatch[i + 1] == null) - { - throw new ArgumentException( - "A wildcard path component must be at the end of the path or followed by a literal path component."); - } - } - - this.wildcardCount = this.isWildcard.Length; - this.IsWildCardPath = this.wildcardCount > 0; - - this.FirstMatchHashKey = !this.IsWildCardPath - ? this.PathComponentsCount + PathSeperator + firstLiteralMatch - : WildCardChar + PathSeperator + firstLiteralMatch; - - this.typeDeserializer = new StringMapTypeDeserializer(createInstanceFn, getParseFn, this.RequestType); - - _propertyNamesMap = new HashSet( - GetSerializableProperties(RequestType).Select(x => x.Name), - StringComparer.OrdinalIgnoreCase); - } - - internal static IEnumerable GetSerializableProperties(Type type) - { - foreach (var prop in GetPublicProperties(type)) - { - if (prop.GetMethod == null - || _excludeType == prop.PropertyType) - { - continue; - } - - var ignored = false; - foreach (var attr in prop.GetCustomAttributes(true)) - { - if (IgnoreAttributesNamed.Contains(attr.GetType().Name)) - { - ignored = true; - break; - } - } - - if (!ignored) - { - yield return prop; - } - } - } - - private static IEnumerable GetPublicProperties(Type type) - { - if (type.IsInterface) - { - var propertyInfos = new List(); - var considered = new List() - { - type - }; - var queue = new Queue(); - queue.Enqueue(type); - - while (queue.Count > 0) - { - var subType = queue.Dequeue(); - foreach (var subInterface in subType.GetTypeInfo().ImplementedInterfaces) - { - if (considered.Contains(subInterface)) - { - continue; - } - - considered.Add(subInterface); - queue.Enqueue(subInterface); - } - - var newPropertyInfos = GetTypesPublicProperties(subType) - .Where(x => !propertyInfos.Contains(x)); - - propertyInfos.InsertRange(0, newPropertyInfos); - } - - return propertyInfos; - } - - return GetTypesPublicProperties(type) - .Where(x => x.GetIndexParameters().Length == 0); - } - - private static IEnumerable GetTypesPublicProperties(Type subType) - { - foreach (var pi in subType.GetRuntimeProperties()) - { - var mi = pi.GetMethod ?? pi.SetMethod; - if (mi != null && mi.IsStatic) - { - continue; - } - - yield return pi; - } - } - - /// - /// Provide for quick lookups based on hashes that can be determined from a request url. - /// - public string FirstMatchHashKey { get; private set; } - - private readonly StringMapTypeDeserializer typeDeserializer; - - private readonly HashSet _propertyNamesMap; - - public int MatchScore(string httpMethod, string[] withPathInfoParts) - { - var isMatch = IsMatch(httpMethod, withPathInfoParts, out var wildcardMatchCount); - if (!isMatch) - { - return -1; - } - - // Routes with least wildcard matches get the highest score - var score = Math.Max(100 - wildcardMatchCount, 1) * 1000 - // Routes with less variable (and more literal) matches - + Math.Max(10 - VariableArgsCount, 1) * 100; - - // Exact verb match is better than ANY - if (Verbs.Length == 1 && string.Equals(httpMethod, Verbs[0], StringComparison.OrdinalIgnoreCase)) - { - score += 10; - } - else - { - score += 1; - } - - return score; - } - - /// - /// For performance withPathInfoParts should already be a lower case string - /// to minimize redundant matching operations. - /// - public bool IsMatch(string httpMethod, string[] withPathInfoParts, out int wildcardMatchCount) - { - wildcardMatchCount = 0; - - if (withPathInfoParts.Length != this.PathComponentsCount && !this.IsWildCardPath) - { - return false; - } - - if (!Verbs.Contains(httpMethod, StringComparer.OrdinalIgnoreCase)) - { - return false; - } - - if (!ExplodeComponents(ref withPathInfoParts)) - { - return false; - } - - if (this.TotalComponentsCount != withPathInfoParts.Length && !this.IsWildCardPath) - { - return false; - } - - int pathIx = 0; - for (var i = 0; i < this.TotalComponentsCount; i++) - { - if (this.isWildcard[i]) - { - if (i < this.TotalComponentsCount - 1) - { - // Continue to consume up until a match with the next literal - while (pathIx < withPathInfoParts.Length - && !string.Equals(withPathInfoParts[pathIx], this.literalsToMatch[i + 1], StringComparison.InvariantCultureIgnoreCase)) - { - pathIx++; - wildcardMatchCount++; - } - - // Ensure there are still enough parts left to match the remainder - if ((withPathInfoParts.Length - pathIx) < (this.TotalComponentsCount - i - 1)) - { - return false; - } - } - else - { - // A wildcard at the end matches the remainder of path - wildcardMatchCount += withPathInfoParts.Length - pathIx; - pathIx = withPathInfoParts.Length; - } - } - else - { - var literalToMatch = this.literalsToMatch[i]; - if (literalToMatch == null) - { - // Matching an ordinary (non-wildcard) variable consumes a single part - pathIx++; - continue; - } - - if (withPathInfoParts.Length <= pathIx - || !string.Equals(withPathInfoParts[pathIx], literalToMatch, StringComparison.InvariantCultureIgnoreCase)) - { - return false; - } - - pathIx++; - } - } - - return pathIx == withPathInfoParts.Length; - } - - private bool ExplodeComponents(ref string[] withPathInfoParts) - { - var totalComponents = new List(); - for (var i = 0; i < withPathInfoParts.Length; i++) - { - var component = withPathInfoParts[i]; - if (string.IsNullOrEmpty(component)) - { - continue; - } - - if (this.PathComponentsCount != this.TotalComponentsCount - && this.componentsWithSeparators[i]) - { - var subComponents = component.Split(ComponentSeperator); - if (subComponents.Length < 2) - { - return false; - } - - totalComponents.AddRange(subComponents); - } - else - { - totalComponents.Add(component); - } - } - - withPathInfoParts = totalComponents.ToArray(); - return true; - } - - public object CreateRequest(string pathInfo, Dictionary queryStringAndFormData, object fromInstance) - { - var requestComponents = pathInfo.Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries); - - ExplodeComponents(ref requestComponents); - - if (requestComponents.Length != this.TotalComponentsCount) - { - var isValidWildCardPath = this.IsWildCardPath - && requestComponents.Length >= this.TotalComponentsCount - this.wildcardCount; - - if (!isValidWildCardPath) - { - throw new ArgumentException( - string.Format( - CultureInfo.InvariantCulture, - "Path Mismatch: Request Path '{0}' has invalid number of components compared to: '{1}'", - pathInfo, - this.restPath)); - } - } - - var requestKeyValuesMap = new Dictionary(); - var pathIx = 0; - for (var i = 0; i < this.TotalComponentsCount; i++) - { - var variableName = this.variablesNames[i]; - if (variableName == null) - { - pathIx++; - continue; - } - - if (!this._propertyNamesMap.Contains(variableName)) - { - if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase)) - { - pathIx++; - continue; - } - - throw new ArgumentException("Could not find property " - + variableName + " on " + RequestType.GetMethodName()); - } - - var value = requestComponents.Length > pathIx ? requestComponents[pathIx] : null; // wildcard has arg mismatch - if (value != null && this.isWildcard[i]) - { - if (i == this.TotalComponentsCount - 1) - { - // Wildcard at end of path definition consumes all the rest - var sb = new StringBuilder(); - sb.Append(value); - for (var j = pathIx + 1; j < requestComponents.Length; j++) - { - sb.Append(PathSeperatorChar) - .Append(requestComponents[j]); - } - - value = sb.ToString(); - } - else - { - // Wildcard in middle of path definition consumes up until it - // hits a match for the next element in the definition (which must be a literal) - // It may consume 0 or more path parts - var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1]; - if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase)) - { - var sb = new StringBuilder(value); - pathIx++; - while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase)) - { - sb.Append(PathSeperatorChar) - .Append(requestComponents[pathIx++]); - } - - value = sb.ToString(); - } - else - { - value = null; - } - } - } - else - { - // Variable consumes single path item - pathIx++; - } - - requestKeyValuesMap[variableName] = value; - } - - if (queryStringAndFormData != null) - { - // Query String and form data can override variable path matches - // path variables < query string < form data - foreach (var name in queryStringAndFormData) - { - requestKeyValuesMap[name.Key] = name.Value; - } - } - - return this.typeDeserializer.PopulateFromMap(fromInstance, requestKeyValuesMap); - } - - public class RestPathMap : SortedDictionary> - { - public RestPathMap() : base(StringComparer.OrdinalIgnoreCase) - { - } - } - } -} diff --git a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs deleted file mode 100644 index 165bb0fc4..000000000 --- a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs +++ /dev/null @@ -1,118 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Reflection; -using MediaBrowser.Common.Extensions; - -namespace Emby.Server.Implementations.Services -{ - /// - /// Serializer cache of delegates required to create a type from a string map (e.g. for REST urls) - /// - public class StringMapTypeDeserializer - { - internal class PropertySerializerEntry - { - public PropertySerializerEntry(Action propertySetFn, Func propertyParseStringFn, Type propertyType) - { - PropertySetFn = propertySetFn; - PropertyParseStringFn = propertyParseStringFn; - PropertyType = propertyType; - } - - public Action PropertySetFn { get; private set; } - - public Func PropertyParseStringFn { get; private set; } - - public Type PropertyType { get; private set; } - } - - private readonly Type type; - private readonly Dictionary propertySetterMap - = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public Func GetParseFn(Type propertyType) - { - if (propertyType == typeof(string)) - { - return s => s; - } - - return _GetParseFn(propertyType); - } - - private readonly Func _CreateInstanceFn; - private readonly Func> _GetParseFn; - - public StringMapTypeDeserializer(Func createInstanceFn, Func> getParseFn, Type type) - { - _CreateInstanceFn = createInstanceFn; - _GetParseFn = getParseFn; - this.type = type; - - foreach (var propertyInfo in RestPath.GetSerializableProperties(type)) - { - var propertySetFn = TypeAccessor.GetSetPropertyMethod(propertyInfo); - var propertyType = propertyInfo.PropertyType; - var propertyParseStringFn = GetParseFn(propertyType); - var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn, propertyType); - - propertySetterMap[propertyInfo.Name] = propertySerializer; - } - } - - public object PopulateFromMap(object instance, IDictionary keyValuePairs) - { - PropertySerializerEntry propertySerializerEntry = null; - - if (instance == null) - { - instance = _CreateInstanceFn(type); - } - - foreach (var pair in keyValuePairs) - { - string propertyName = pair.Key; - string propertyTextValue = pair.Value; - - if (propertyTextValue == null - || !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry) - || propertySerializerEntry.PropertySetFn == null) - { - continue; - } - - if (propertySerializerEntry.PropertyType == typeof(bool)) - { - // InputExtensions.cs#530 MVC Checkbox helper emits extra hidden input field, generating 2 values, first is the real value - propertyTextValue = StringExtensions.LeftPart(propertyTextValue, ',').ToString(); - } - - var value = propertySerializerEntry.PropertyParseStringFn(propertyTextValue); - if (value == null) - { - continue; - } - - propertySerializerEntry.PropertySetFn(instance, value); - } - - return instance; - } - } - - internal static class TypeAccessor - { - public static Action GetSetPropertyMethod(PropertyInfo propertyInfo) - { - if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0) - { - return null; - } - - var setMethodInfo = propertyInfo.SetMethod; - return (instance, value) => setMethodInfo.Invoke(instance, new[] { value }); - } - } -} diff --git a/Emby.Server.Implementations/Services/UrlExtensions.cs b/Emby.Server.Implementations/Services/UrlExtensions.cs deleted file mode 100644 index 92e36b60e..000000000 --- a/Emby.Server.Implementations/Services/UrlExtensions.cs +++ /dev/null @@ -1,27 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using MediaBrowser.Common.Extensions; - -namespace Emby.Server.Implementations.Services -{ - /// - /// Donated by Ivan Korneliuk from his post: - /// http://korneliuk.blogspot.com/2012/08/servicestack-reusing-dtos.html - /// - /// Modified to only allow using routes matching the supplied HTTP Verb. - /// - public static class UrlExtensions - { - public static string GetMethodName(this Type type) - { - var typeName = type.FullName != null // can be null, e.g. generic types - ? StringExtensions.LeftPart(type.FullName, "[[", StringComparison.Ordinal).ToString() // Generic Fullname - .Replace(type.Namespace + ".", string.Empty, StringComparison.Ordinal) // Trim Namespaces - .Replace("+", ".", StringComparison.Ordinal) // Convert nested into normal type - : type.Name; - - return type.IsGenericParameter ? "'" + typeName : typeName; - } - } -} diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs deleted file mode 100644 index ae1a8d0b7..000000000 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ /dev/null @@ -1,248 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Mime; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; -using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest; - -namespace Emby.Server.Implementations.SocketSharp -{ - public class WebSocketSharpRequest : IHttpRequest - { - private const string FormUrlEncoded = "application/x-www-form-urlencoded"; - private const string MultiPartFormData = "multipart/form-data"; - private const string Soap11 = "text/xml; charset=utf-8"; - - private string _remoteIp; - private Dictionary _items; - private string _responseContentType; - - public WebSocketSharpRequest(HttpRequest httpRequest, HttpResponse httpResponse, string operationName) - { - this.OperationName = operationName; - this.Request = httpRequest; - this.Response = httpResponse; - } - - public string Accept => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Accept]) ? null : Request.Headers[HeaderNames.Accept].ToString(); - - public string Authorization => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Authorization]) ? null : Request.Headers[HeaderNames.Authorization].ToString(); - - public HttpRequest Request { get; } - - public HttpResponse Response { get; } - - public string OperationName { get; set; } - - public string RawUrl => Request.GetEncodedPathAndQuery(); - - public string AbsoluteUri => Request.GetDisplayUrl().TrimEnd('/'); - - public string RemoteIp - { - get - { - if (_remoteIp != null) - { - return _remoteIp; - } - - IPAddress ip; - - // "Real" remote ip might be in X-Forwarded-For of X-Real-Ip - // (if the server is behind a reverse proxy for example) - if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XForwardedFor), out ip)) - { - if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XRealIP), out ip)) - { - ip = Request.HttpContext.Connection.RemoteIpAddress; - - // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests) - ip ??= IPAddress.Loopback; - } - } - - return _remoteIp = NormalizeIp(ip).ToString(); - } - } - - public string[] AcceptTypes => Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept); - - public Dictionary Items => _items ?? (_items = new Dictionary()); - - public string ResponseContentType - { - get => - _responseContentType - ?? (_responseContentType = GetResponseContentType(Request)); - set => _responseContentType = value; - } - - public string PathInfo => Request.Path.Value; - - public string UserAgent => Request.Headers[HeaderNames.UserAgent]; - - public IHeaderDictionary Headers => Request.Headers; - - public IQueryCollection QueryString => Request.Query; - - public bool IsLocal => - (Request.HttpContext.Connection.LocalIpAddress == null - && Request.HttpContext.Connection.RemoteIpAddress == null) - || Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress); - - public string HttpMethod => Request.Method; - - public string Verb => HttpMethod; - - public string ContentType => Request.ContentType; - - public Uri UrlReferrer => Request.GetTypedHeaders().Referer; - - public Stream InputStream => Request.Body; - - public long ContentLength => Request.ContentLength ?? 0; - - private string GetHeader(string name) => Request.Headers[name].ToString(); - - private static IPAddress NormalizeIp(IPAddress ip) - { - if (ip.IsIPv4MappedToIPv6) - { - return ip.MapToIPv4(); - } - - return ip; - } - - public static string GetResponseContentType(HttpRequest httpReq) - { - var specifiedContentType = GetQueryStringContentType(httpReq); - if (!string.IsNullOrEmpty(specifiedContentType)) - { - return specifiedContentType; - } - - const string ServerDefaultContentType = MediaTypeNames.Application.Json; - - var acceptContentTypes = httpReq.Headers.GetCommaSeparatedValues(HeaderNames.Accept); - string defaultContentType = null; - if (HasAnyOfContentTypes(httpReq, FormUrlEncoded, MultiPartFormData)) - { - defaultContentType = ServerDefaultContentType; - } - - var acceptsAnything = false; - var hasDefaultContentType = defaultContentType != null; - if (acceptContentTypes != null) - { - foreach (ReadOnlySpan acceptsType in acceptContentTypes) - { - ReadOnlySpan contentType = acceptsType; - var index = contentType.IndexOf(';'); - if (index != -1) - { - contentType = contentType.Slice(0, index); - } - - contentType = contentType.Trim(); - acceptsAnything = contentType.Equals("*/*", StringComparison.OrdinalIgnoreCase); - - if (acceptsAnything) - { - break; - } - } - - if (acceptsAnything) - { - if (hasDefaultContentType) - { - return defaultContentType; - } - else - { - return ServerDefaultContentType; - } - } - } - - if (acceptContentTypes == null && httpReq.ContentType == Soap11) - { - return Soap11; - } - - // We could also send a '406 Not Acceptable', but this is allowed also - return ServerDefaultContentType; - } - - public static bool HasAnyOfContentTypes(HttpRequest request, params string[] contentTypes) - { - if (contentTypes == null || request.ContentType == null) - { - return false; - } - - foreach (var contentType in contentTypes) - { - if (IsContentType(request, contentType)) - { - return true; - } - } - - return false; - } - - public static bool IsContentType(HttpRequest request, string contentType) - { - return request.ContentType.StartsWith(contentType, StringComparison.OrdinalIgnoreCase); - } - - private static string GetQueryStringContentType(HttpRequest httpReq) - { - ReadOnlySpan format = httpReq.Query["format"].ToString(); - if (format == ReadOnlySpan.Empty) - { - const int FormatMaxLength = 4; - ReadOnlySpan pi = httpReq.Path.ToString(); - if (pi == null || pi.Length <= FormatMaxLength) - { - return null; - } - - if (pi[0] == '/') - { - pi = pi.Slice(1); - } - - format = pi.LeftPart('/'); - if (format.Length > FormatMaxLength) - { - return null; - } - } - - format = format.LeftPart('.'); - if (format.Contains("json", StringComparison.OrdinalIgnoreCase)) - { - return "application/json"; - } - else if (format.Contains("xml", StringComparison.OrdinalIgnoreCase)) - { - return "application/xml"; - } - - return null; - } - } -} diff --git a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs index d746207c7..76db68885 100644 --- a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs +++ b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Model.Services; +using System.Net; +using MediaBrowser.Common.Net; using Microsoft.AspNetCore.Http; namespace MediaBrowser.Common.Extensions @@ -8,26 +9,54 @@ namespace MediaBrowser.Common.Extensions /// public static class HttpContextExtensions { - private const string ServiceStackRequest = "ServiceStackRequest"; - /// - /// Set the ServiceStack request. + /// Checks the origin of the HTTP request. /// - /// The HttpContext instance. - /// The service stack request instance. - public static void SetServiceStackRequest(this HttpContext httpContext, IRequest request) + /// The incoming HTTP request. + /// true if the request is coming from LAN, false otherwise. + public static bool IsLocal(this HttpRequest request) { - httpContext.Items[ServiceStackRequest] = request; + return (request.HttpContext.Connection.LocalIpAddress == null + && request.HttpContext.Connection.RemoteIpAddress == null) + || request.HttpContext.Connection.LocalIpAddress.Equals(request.HttpContext.Connection.RemoteIpAddress); } /// - /// Get the ServiceStack request. + /// Extracts the remote IP address of the caller of the HTTP request. /// - /// The HttpContext instance. - /// The service stack request instance. - public static IRequest GetServiceStackRequest(this HttpContext httpContext) + /// The HTTP request. + /// The remote caller IP address. + public static string RemoteIp(this HttpRequest request) { - return (IRequest)httpContext.Items[ServiceStackRequest]; + if (string.IsNullOrEmpty(request.HttpContext.Items["RemoteIp"].ToString())) + { + return request.HttpContext.Items["RemoteIp"].ToString(); + } + + IPAddress ip; + + // "Real" remote ip might be in X-Forwarded-For of X-Real-Ip + // (if the server is behind a reverse proxy for example) + if (!IPAddress.TryParse(request.Headers[CustomHeaderNames.XForwardedFor].ToString(), out ip)) + { + if (!IPAddress.TryParse(request.Headers[CustomHeaderNames.XRealIP].ToString(), out ip)) + { + ip = request.HttpContext.Connection.RemoteIpAddress; + + // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests) + ip ??= IPAddress.Loopback; + } + } + + if (ip.IsIPv4MappedToIPv6) + { + ip = ip.MapToIPv4(); + } + + var normalizedIp = ip.ToString(); + + request.HttpContext.Items["RemoteIp"] = normalizedIp; + return normalizedIp; } } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs index 4cbb63e46..1f3abe8f4 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Services; namespace MediaBrowser.Controller.MediaEncoding { @@ -63,26 +62,20 @@ namespace MediaBrowser.Controller.MediaEncoding /// Gets or sets the id. /// /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public Guid Id { get; set; } - [ApiMember(Name = "MediaSourceId", Description = "The media version id, if playing an alternate version", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public string MediaSourceId { get; set; } - [ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string DeviceId { get; set; } - [ApiMember(Name = "Container", Description = "Container", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] public string Container { get; set; } /// /// Gets or sets the audio codec. /// /// The audio codec. - [ApiMember(Name = "AudioCodec", Description = "Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string AudioCodec { get; set; } - [ApiMember(Name = "EnableAutoStreamCopy", Description = "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] public bool EnableAutoStreamCopy { get; set; } public bool AllowVideoStreamCopy { get; set; } @@ -95,7 +88,6 @@ namespace MediaBrowser.Controller.MediaEncoding /// Gets or sets the audio sample rate. /// /// The audio sample rate. - [ApiMember(Name = "AudioSampleRate", Description = "Optional. Specify a specific audio sample rate, e.g. 44100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? AudioSampleRate { get; set; } public int? MaxAudioBitDepth { get; set; } @@ -104,105 +96,86 @@ namespace MediaBrowser.Controller.MediaEncoding /// Gets or sets the audio bit rate. /// /// The audio bit rate. - [ApiMember(Name = "AudioBitRate", Description = "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? AudioBitRate { get; set; } /// /// Gets or sets the audio channels. /// /// The audio channels. - [ApiMember(Name = "AudioChannels", Description = "Optional. Specify a specific number of audio channels to encode to, e.g. 2", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? AudioChannels { get; set; } - [ApiMember(Name = "MaxAudioChannels", Description = "Optional. Specify a maximum number of audio channels to encode to, e.g. 2", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? MaxAudioChannels { get; set; } - [ApiMember(Name = "Static", Description = "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] public bool Static { get; set; } /// /// Gets or sets the profile. /// /// The profile. - [ApiMember(Name = "Profile", Description = "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string Profile { get; set; } /// /// Gets or sets the level. /// /// The level. - [ApiMember(Name = "Level", Description = "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string Level { get; set; } /// /// Gets or sets the framerate. /// /// The framerate. - [ApiMember(Name = "Framerate", Description = "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", IsRequired = false, DataType = "double", ParameterType = "query", Verb = "GET")] public float? Framerate { get; set; } - [ApiMember(Name = "MaxFramerate", Description = "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", IsRequired = false, DataType = "double", ParameterType = "query", Verb = "GET")] public float? MaxFramerate { get; set; } - [ApiMember(Name = "CopyTimestamps", Description = "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] public bool CopyTimestamps { get; set; } /// /// Gets or sets the start time ticks. /// /// The start time ticks. - [ApiMember(Name = "StartTimeTicks", Description = "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public long? StartTimeTicks { get; set; } /// /// Gets or sets the width. /// /// The width. - [ApiMember(Name = "Width", Description = "Optional. The fixed horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? Width { get; set; } /// /// Gets or sets the height. /// /// The height. - [ApiMember(Name = "Height", Description = "Optional. The fixed vertical resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? Height { get; set; } /// /// Gets or sets the width of the max. /// /// The width of the max. - [ApiMember(Name = "MaxWidth", Description = "Optional. The maximum horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? MaxWidth { get; set; } /// /// Gets or sets the height of the max. /// /// The height of the max. - [ApiMember(Name = "MaxHeight", Description = "Optional. The maximum vertical resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? MaxHeight { get; set; } /// /// Gets or sets the video bit rate. /// /// The video bit rate. - [ApiMember(Name = "VideoBitRate", Description = "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? VideoBitRate { get; set; } /// /// Gets or sets the index of the subtitle stream. /// /// The index of the subtitle stream. - [ApiMember(Name = "SubtitleStreamIndex", Description = "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? SubtitleStreamIndex { get; set; } - [ApiMember(Name = "SubtitleMethod", Description = "Optional. Specify the subtitle delivery method.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public SubtitleDeliveryMethod SubtitleMethod { get; set; } - [ApiMember(Name = "MaxRefFrames", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? MaxRefFrames { get; set; } - [ApiMember(Name = "MaxVideoBitDepth", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? MaxVideoBitDepth { get; set; } public bool RequireAvc { get; set; } @@ -223,7 +196,6 @@ namespace MediaBrowser.Controller.MediaEncoding /// Gets or sets the video codec. /// /// The video codec. - [ApiMember(Name = "VideoCodec", Description = "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string VideoCodec { get; set; } public string SubtitleCodec { get; set; } @@ -234,14 +206,12 @@ namespace MediaBrowser.Controller.MediaEncoding /// Gets or sets the index of the audio stream. /// /// The index of the audio stream. - [ApiMember(Name = "AudioStreamIndex", Description = "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? AudioStreamIndex { get; set; } /// /// Gets or sets the index of the video stream. /// /// The index of the video stream. - [ApiMember(Name = "VideoStreamIndex", Description = "Optional. The index of the video stream to use. If omitted the first video stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? VideoStreamIndex { get; set; } public EncodingContext Context { get; set; } diff --git a/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs b/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs deleted file mode 100644 index 1366fd42e..000000000 --- a/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs +++ /dev/null @@ -1,76 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; - -namespace MediaBrowser.Controller.Net -{ - public class AuthenticatedAttribute : Attribute, IHasRequestFilter, IAuthenticationAttributes - { - public static IAuthService AuthService { get; set; } - - /// - /// Gets or sets the roles. - /// - /// The roles. - public string Roles { get; set; } - - /// - /// Gets or sets a value indicating whether [escape parental control]. - /// - /// true if [escape parental control]; otherwise, false. - public bool EscapeParentalControl { get; set; } - - /// - /// Gets or sets a value indicating whether [allow before startup wizard]. - /// - /// true if [allow before startup wizard]; otherwise, false. - public bool AllowBeforeStartupWizard { get; set; } - - public bool AllowLocal { get; set; } - - /// - /// The request filter is executed before the service. - /// - /// The http request wrapper. - /// The http response wrapper. - /// The request DTO. - public void RequestFilter(IRequest request, HttpResponse response, object requestDto) - { - AuthService.Authenticate(request, this); - } - - /// - /// Order in which Request Filters are executed. - /// <0 Executed before global request filters - /// >0 Executed after global request filters - /// - /// The priority. - public int Priority => 0; - - public string[] GetRoles() - { - return (Roles ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - } - - public bool IgnoreLegacyAuth { get; set; } - - public bool AllowLocalOnly { get; set; } - } - - public interface IAuthenticationAttributes - { - bool EscapeParentalControl { get; } - - bool AllowBeforeStartupWizard { get; } - - bool AllowLocal { get; } - - bool AllowLocalOnly { get; } - - string[] GetRoles(); - - bool IgnoreLegacyAuth { get; } - } -} diff --git a/MediaBrowser.Controller/Net/IAuthService.cs b/MediaBrowser.Controller/Net/IAuthService.cs index 2055a656a..04b2e13e8 100644 --- a/MediaBrowser.Controller/Net/IAuthService.cs +++ b/MediaBrowser.Controller/Net/IAuthService.cs @@ -1,7 +1,5 @@ #nullable enable -using Jellyfin.Data.Entities; -using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net @@ -11,21 +9,6 @@ namespace MediaBrowser.Controller.Net /// public interface IAuthService { - /// - /// Authenticate and authorize request. - /// - /// Request. - /// Authorization attributes. - void Authenticate(IRequest request, IAuthenticationAttributes authAttribtutes); - - /// - /// Authenticate and authorize request. - /// - /// Request. - /// Authorization attributes. - /// Authenticated user. - User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtutes); - /// /// Authenticate request. /// diff --git a/MediaBrowser.Controller/Net/IAuthorizationContext.cs b/MediaBrowser.Controller/Net/IAuthorizationContext.cs index 37a7425b9..4d2f5f5e3 100644 --- a/MediaBrowser.Controller/Net/IAuthorizationContext.cs +++ b/MediaBrowser.Controller/Net/IAuthorizationContext.cs @@ -1,4 +1,3 @@ -using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net @@ -20,7 +19,7 @@ namespace MediaBrowser.Controller.Net /// /// The request context. /// AuthorizationInfo. - AuthorizationInfo GetAuthorizationInfo(IRequest requestContext); + AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext); /// /// Gets the authorization information. diff --git a/MediaBrowser.Controller/Net/IHasResultFactory.cs b/MediaBrowser.Controller/Net/IHasResultFactory.cs deleted file mode 100644 index b8cf8cd78..000000000 --- a/MediaBrowser.Controller/Net/IHasResultFactory.cs +++ /dev/null @@ -1,17 +0,0 @@ -using MediaBrowser.Model.Services; - -namespace MediaBrowser.Controller.Net -{ - /// - /// Interface IHasResultFactory - /// Services that require a ResultFactory should implement this - /// - public interface IHasResultFactory : IRequiresRequest - { - /// - /// Gets or sets the result factory. - /// - /// The result factory. - IHttpResultFactory ResultFactory { get; set; } - } -} diff --git a/MediaBrowser.Controller/Net/IHttpResultFactory.cs b/MediaBrowser.Controller/Net/IHttpResultFactory.cs deleted file mode 100644 index 8293a8714..000000000 --- a/MediaBrowser.Controller/Net/IHttpResultFactory.cs +++ /dev/null @@ -1,82 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; - -namespace MediaBrowser.Controller.Net -{ - /// - /// Interface IHttpResultFactory. - /// - public interface IHttpResultFactory - { - /// - /// Gets the result. - /// - /// The content. - /// Type of the content. - /// The response headers. - /// System.Object. - object GetResult(string content, string contentType, IDictionary responseHeaders = null); - - object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary responseHeaders = null); - - object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary responseHeaders = null); - - object GetResult(IRequest requestContext, string content, string contentType, IDictionary responseHeaders = null); - - object GetRedirectResult(string url); - - object GetResult(IRequest requestContext, T result, IDictionary responseHeaders = null) - where T : class; - - /// - /// Gets the static result. - /// - /// The request context. - /// The cache key. - /// The last date modified. - /// Duration of the cache. - /// Type of the content. - /// The factory fn. - /// The response headers. - /// if set to true [is head request]. - /// System.Object. - Task GetStaticResult(IRequest requestContext, - Guid cacheKey, - DateTime? lastDateModified, - TimeSpan? cacheDuration, - string contentType, Func> factoryFn, - IDictionary responseHeaders = null, - bool isHeadRequest = false); - - /// - /// Gets the static result. - /// - /// The request context. - /// The options. - /// System.Object. - Task GetStaticResult(IRequest requestContext, StaticResultOptions options); - - /// - /// Gets the static file result. - /// - /// The request context. - /// The path. - /// The file share. - /// System.Object. - Task GetStaticFileResult(IRequest requestContext, string path, FileShare fileShare = FileShare.Read); - - /// - /// Gets the static file result. - /// - /// The request context. - /// The options. - /// System.Object. - Task GetStaticFileResult(IRequest requestContext, - StaticFileResultOptions options); - } -} diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs index b04ebda8c..845f27045 100644 --- a/MediaBrowser.Controller/Net/IHttpServer.cs +++ b/MediaBrowser.Controller/Net/IHttpServer.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Jellyfin.Data.Events; -using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net @@ -26,7 +25,7 @@ namespace MediaBrowser.Controller.Net /// /// Inits this instance. /// - void Init(IEnumerable serviceTypes, IEnumerable listener, IEnumerable urlPrefixes); + void Init(IEnumerable listener, IEnumerable urlPrefixes); /// /// If set, all requests will respond with this message. @@ -43,8 +42,8 @@ namespace MediaBrowser.Controller.Net /// /// Get the default CORS headers. /// - /// - /// - IDictionary GetDefaultCorsHeaders(IRequest req); + /// The HTTP context of the current request. + /// The default CORS headers for the context. + IDictionary GetDefaultCorsHeaders(HttpContext httpContext); } } diff --git a/MediaBrowser.Controller/Net/ISessionContext.cs b/MediaBrowser.Controller/Net/ISessionContext.cs index 5da748f41..a60dc2ea1 100644 --- a/MediaBrowser.Controller/Net/ISessionContext.cs +++ b/MediaBrowser.Controller/Net/ISessionContext.cs @@ -2,7 +2,7 @@ using Jellyfin.Data.Entities; using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net { @@ -12,8 +12,8 @@ namespace MediaBrowser.Controller.Net User GetUser(object requestContext); - SessionInfo GetSession(IRequest requestContext); + SessionInfo GetSession(HttpContext requestContext); - User GetUser(IRequest requestContext); + User GetUser(HttpContext requestContext); } } diff --git a/MediaBrowser.Controller/Net/StaticResultOptions.cs b/MediaBrowser.Controller/Net/StaticResultOptions.cs deleted file mode 100644 index c1e9bc845..000000000 --- a/MediaBrowser.Controller/Net/StaticResultOptions.cs +++ /dev/null @@ -1,44 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -namespace MediaBrowser.Controller.Net -{ - public class StaticResultOptions - { - public string ContentType { get; set; } - - public TimeSpan? CacheDuration { get; set; } - - public DateTime? DateLastModified { get; set; } - - public Func> ContentFactory { get; set; } - - public bool IsHeadRequest { get; set; } - - public IDictionary ResponseHeaders { get; set; } - - public Action OnComplete { get; set; } - - public Action OnError { get; set; } - - public string Path { get; set; } - - public long? ContentLength { get; set; } - - public FileShare FileShare { get; set; } - - public StaticResultOptions() - { - ResponseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - FileShare = FileShare.Read; - } - } - - public class StaticFileResultOptions : StaticResultOptions - { - } -} diff --git a/MediaBrowser.Model/Services/ApiMemberAttribute.cs b/MediaBrowser.Model/Services/ApiMemberAttribute.cs deleted file mode 100644 index 63f3ecd55..000000000 --- a/MediaBrowser.Model/Services/ApiMemberAttribute.cs +++ /dev/null @@ -1,65 +0,0 @@ -#nullable disable -using System; - -namespace MediaBrowser.Model.Services -{ - /// - /// Identifies a single API endpoint. - /// - [AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)] - public class ApiMemberAttribute : Attribute - { - /// - /// Gets or sets verb to which applies attribute. By default applies to all verbs. - /// - public string Verb { get; set; } - - /// - /// Gets or sets parameter type: It can be only one of the following: path, query, body, form, or header. - /// - public string ParameterType { get; set; } - - /// - /// Gets or sets unique name for the parameter. Each name must be unique, even if they are associated with different paramType values. - /// - /// - /// - /// Other notes on the name field: - /// If paramType is body, the name is used only for UI and codegeneration. - /// If paramType is path, the name field must correspond to the associated path segment from the path field in the api object. - /// If paramType is query, the name field corresponds to the query param name. - /// - /// - public string Name { get; set; } - - /// - /// Gets or sets the human-readable description for the parameter. - /// - public string Description { get; set; } - - /// - /// For path, query, and header paramTypes, this field must be a primitive. For body, this can be a complex or container datatype. - /// - public string DataType { get; set; } - - /// - /// For path, this is always true. Otherwise, this field tells the client whether or not the field must be supplied. - /// - public bool IsRequired { get; set; } - - /// - /// For query params, this specifies that a comma-separated list of values can be passed to the API. For path and body types, this field cannot be true. - /// - public bool AllowMultiple { get; set; } - - /// - /// Gets or sets route to which applies attribute, matches using StartsWith. By default applies to all routes. - /// - public string Route { get; set; } - - /// - /// Whether to exclude this property from being included in the ModelSchema. - /// - public bool ExcludeInSchema { get; set; } - } -} diff --git a/MediaBrowser.Model/Services/IAsyncStreamWriter.cs b/MediaBrowser.Model/Services/IAsyncStreamWriter.cs deleted file mode 100644 index afbca78a2..000000000 --- a/MediaBrowser.Model/Services/IAsyncStreamWriter.cs +++ /dev/null @@ -1,13 +0,0 @@ -#pragma warning disable CS1591 - -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Model.Services -{ - public interface IAsyncStreamWriter - { - Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken); - } -} diff --git a/MediaBrowser.Model/Services/IHasHeaders.cs b/MediaBrowser.Model/Services/IHasHeaders.cs deleted file mode 100644 index 313f34b41..000000000 --- a/MediaBrowser.Model/Services/IHasHeaders.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace MediaBrowser.Model.Services -{ - public interface IHasHeaders - { - IDictionary Headers { get; } - } -} diff --git a/MediaBrowser.Model/Services/IHasRequestFilter.cs b/MediaBrowser.Model/Services/IHasRequestFilter.cs deleted file mode 100644 index b83d3b075..000000000 --- a/MediaBrowser.Model/Services/IHasRequestFilter.cs +++ /dev/null @@ -1,24 +0,0 @@ -#pragma warning disable CS1591 - -using Microsoft.AspNetCore.Http; - -namespace MediaBrowser.Model.Services -{ - public interface IHasRequestFilter - { - /// - /// Gets the order in which Request Filters are executed. - /// <0 Executed before global request filters. - /// >0 Executed after global request filters. - /// - int Priority { get; } - - /// - /// The request filter is executed before the service. - /// - /// The http request wrapper. - /// The http response wrapper. - /// The request DTO. - void RequestFilter(IRequest req, HttpResponse res, object requestDto); - } -} diff --git a/MediaBrowser.Model/Services/IHttpRequest.cs b/MediaBrowser.Model/Services/IHttpRequest.cs deleted file mode 100644 index 3ea65195c..000000000 --- a/MediaBrowser.Model/Services/IHttpRequest.cs +++ /dev/null @@ -1,17 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Services -{ - public interface IHttpRequest : IRequest - { - /// - /// Gets the HTTP Verb. - /// - string HttpMethod { get; } - - /// - /// Gets the value of the Accept HTTP Request Header. - /// - string Accept { get; } - } -} diff --git a/MediaBrowser.Model/Services/IHttpResult.cs b/MediaBrowser.Model/Services/IHttpResult.cs deleted file mode 100644 index abc581d8e..000000000 --- a/MediaBrowser.Model/Services/IHttpResult.cs +++ /dev/null @@ -1,35 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System.Net; - -namespace MediaBrowser.Model.Services -{ - public interface IHttpResult : IHasHeaders - { - /// - /// The HTTP Response Status. - /// - int Status { get; set; } - - /// - /// The HTTP Response Status Code. - /// - HttpStatusCode StatusCode { get; set; } - - /// - /// The HTTP Response ContentType. - /// - string ContentType { get; set; } - - /// - /// Response DTO. - /// - object Response { get; set; } - - /// - /// Holds the request call context. - /// - IRequest RequestContext { get; set; } - } -} diff --git a/MediaBrowser.Model/Services/IRequest.cs b/MediaBrowser.Model/Services/IRequest.cs deleted file mode 100644 index 8bc1d3668..000000000 --- a/MediaBrowser.Model/Services/IRequest.cs +++ /dev/null @@ -1,93 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.IO; -using Microsoft.AspNetCore.Http; - -namespace MediaBrowser.Model.Services -{ - public interface IRequest - { - HttpResponse Response { get; } - - /// - /// The name of the service being called (e.g. Request DTO Name) - /// - string OperationName { get; set; } - - /// - /// The Verb / HttpMethod or Action for this request - /// - string Verb { get; } - - /// - /// The request ContentType. - /// - string ContentType { get; } - - bool IsLocal { get; } - - string UserAgent { get; } - - /// - /// The expected Response ContentType for this request. - /// - string ResponseContentType { get; set; } - - /// - /// Attach any data to this request that all filters and services can access. - /// - Dictionary Items { get; } - - IHeaderDictionary Headers { get; } - - IQueryCollection QueryString { get; } - - string RawUrl { get; } - - string AbsoluteUri { get; } - - /// - /// The Remote Ip as reported by X-Forwarded-For, X-Real-IP or Request.UserHostAddress - /// - string RemoteIp { get; } - - /// - /// The value of the Authorization Header used to send the Api Key, null if not available. - /// - string Authorization { get; } - - string[] AcceptTypes { get; } - - string PathInfo { get; } - - Stream InputStream { get; } - - long ContentLength { get; } - - /// - /// The value of the Referrer, null if not available. - /// - Uri UrlReferrer { get; } - } - - public interface IHttpFile - { - string Name { get; } - - string FileName { get; } - - long ContentLength { get; } - - string ContentType { get; } - - Stream InputStream { get; } - } - - public interface IRequiresRequest - { - IRequest Request { get; set; } - } -} diff --git a/MediaBrowser.Model/Services/IRequiresRequestStream.cs b/MediaBrowser.Model/Services/IRequiresRequestStream.cs deleted file mode 100644 index 3e5f2da42..000000000 --- a/MediaBrowser.Model/Services/IRequiresRequestStream.cs +++ /dev/null @@ -1,14 +0,0 @@ -#pragma warning disable CS1591 - -using System.IO; - -namespace MediaBrowser.Model.Services -{ - public interface IRequiresRequestStream - { - /// - /// The raw Http Request Input Stream. - /// - Stream RequestStream { get; set; } - } -} diff --git a/MediaBrowser.Model/Services/IService.cs b/MediaBrowser.Model/Services/IService.cs deleted file mode 100644 index 5233f57ab..000000000 --- a/MediaBrowser.Model/Services/IService.cs +++ /dev/null @@ -1,15 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Services -{ - // marker interface - public interface IService - { - } - - public interface IReturn { } - - public interface IReturn : IReturn { } - - public interface IReturnVoid : IReturn { } -} diff --git a/MediaBrowser.Model/Services/IStreamWriter.cs b/MediaBrowser.Model/Services/IStreamWriter.cs deleted file mode 100644 index 3ebfef66b..000000000 --- a/MediaBrowser.Model/Services/IStreamWriter.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -using System.IO; - -namespace MediaBrowser.Model.Services -{ - public interface IStreamWriter - { - void WriteTo(Stream responseStream); - } -} diff --git a/MediaBrowser.Model/Services/QueryParamCollection.cs b/MediaBrowser.Model/Services/QueryParamCollection.cs deleted file mode 100644 index bdb0cabdf..000000000 --- a/MediaBrowser.Model/Services/QueryParamCollection.cs +++ /dev/null @@ -1,147 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Linq; -using MediaBrowser.Model.Dto; - -namespace MediaBrowser.Model.Services -{ - // Remove this garbage class, it's just a bastard copy of NameValueCollection - public class QueryParamCollection : List - { - public QueryParamCollection() - { - } - - private static StringComparison GetStringComparison() - { - return StringComparison.OrdinalIgnoreCase; - } - - /// - /// Adds a new query parameter. - /// - public void Add(string key, string value) - { - Add(new NameValuePair(key, value)); - } - - private void Set(string key, string value) - { - if (string.IsNullOrEmpty(value)) - { - var parameters = GetItems(key); - - foreach (var p in parameters) - { - Remove(p); - } - - return; - } - - foreach (var pair in this) - { - var stringComparison = GetStringComparison(); - - if (string.Equals(key, pair.Name, stringComparison)) - { - pair.Value = value; - return; - } - } - - Add(key, value); - } - - private string Get(string name) - { - var stringComparison = GetStringComparison(); - - foreach (var pair in this) - { - if (string.Equals(pair.Name, name, stringComparison)) - { - return pair.Value; - } - } - - return null; - } - - private List GetItems(string name) - { - var stringComparison = GetStringComparison(); - - var list = new List(); - - foreach (var pair in this) - { - if (string.Equals(pair.Name, name, stringComparison)) - { - list.Add(pair); - } - } - - return list; - } - - public virtual List GetValues(string name) - { - var stringComparison = GetStringComparison(); - - var list = new List(); - - foreach (var pair in this) - { - if (string.Equals(pair.Name, name, stringComparison)) - { - list.Add(pair.Value); - } - } - - return list; - } - - public IEnumerable Keys - { - get - { - var keys = new string[this.Count]; - - for (var i = 0; i < keys.Length; i++) - { - keys[i] = this[i].Name; - } - - return keys; - } - } - - /// - /// Gets or sets a query parameter value by name. A query may contain multiple values of the same name - /// (i.e. "x=1&x=2"), in which case the value is an array, which works for both getting and setting. - /// - /// The query parameter name. - /// The query parameter value or array of values. - public string this[string name] - { - get => Get(name); - set => Set(name, value); - } - - private string GetQueryStringValue(NameValuePair pair) - { - return pair.Name + "=" + pair.Value; - } - - public override string ToString() - { - var vals = this.Select(GetQueryStringValue).ToArray(); - - return string.Join("&", vals); - } - } -} diff --git a/MediaBrowser.Model/Services/RouteAttribute.cs b/MediaBrowser.Model/Services/RouteAttribute.cs deleted file mode 100644 index f8bf51112..000000000 --- a/MediaBrowser.Model/Services/RouteAttribute.cs +++ /dev/null @@ -1,163 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; - -namespace MediaBrowser.Model.Services -{ - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] - public class RouteAttribute : Attribute - { - /// - /// Initializes an instance of the class. - /// - /// - /// The path template to map to the request. See - /// RouteAttribute.Path - /// for details on the correct format. - /// - public RouteAttribute(string path) - : this(path, null) - { - } - - /// - /// Initializes an instance of the class. - /// - /// - /// The path template to map to the request. See - /// RouteAttribute.Path - /// for details on the correct format. - /// - /// A comma-delimited list of HTTP verbs supported by the - /// service. If unspecified, all verbs are assumed to be supported. - public RouteAttribute(string path, string verbs) - { - Path = path; - Verbs = verbs; - } - - /// - /// Gets or sets the path template to be mapped to the request. - /// - /// - /// A value providing the path mapped to - /// the request. Never . - /// - /// - /// Some examples of valid paths are: - /// - /// - /// "/Inventory" - /// "/Inventory/{Category}/{ItemId}" - /// "/Inventory/{ItemPath*}" - /// - /// - /// Variables are specified within "{}" - /// brackets. Each variable in the path is mapped to the same-named property - /// on the request DTO. At runtime, ServiceStack will parse the - /// request URL, extract the variable values, instantiate the request DTO, - /// and assign the variable values into the corresponding request properties, - /// prior to passing the request DTO to the service object for processing. - /// - /// It is not necessary to specify all request properties as - /// variables in the path. For unspecified properties, callers may provide - /// values in the query string. For example: the URL - /// "http://services/Inventory?Category=Books&ItemId=12345" causes the same - /// request DTO to be processed as "http://services/Inventory/Books/12345", - /// provided that the paths "/Inventory" (which supports the first URL) and - /// "/Inventory/{Category}/{ItemId}" (which supports the second URL) - /// are both mapped to the request DTO. - /// - /// Please note that while it is possible to specify property values - /// in the query string, it is generally considered to be less RESTful and - /// less desirable than to specify them as variables in the path. Using the - /// query string to specify property values may also interfere with HTTP - /// caching. - /// - /// The final variable in the path may contain a "*" suffix - /// to grab all remaining segments in the path portion of the request URL and assign - /// them to a single property on the request DTO. - /// For example, if the path "/Inventory/{ItemPath*}" is mapped to the request DTO, - /// then the request URL "http://services/Inventory/Books/12345" will result - /// in a request DTO whose ItemPath property contains "Books/12345". - /// You may only specify one such variable in the path, and it must be positioned at - /// the end of the path. - /// - public string Path { get; set; } - - /// - /// Gets or sets short summary of what the route does. - /// - public string Summary { get; set; } - - public string Description { get; set; } - - public bool IsHidden { get; set; } - - /// - /// Gets or sets longer text to explain the behaviour of the route. - /// - public string Notes { get; set; } - - /// - /// Gets or sets a comma-delimited list of HTTP verbs supported by the service, such as - /// "GET,PUT,POST,DELETE". - /// - /// - /// A providing a comma-delimited list of HTTP verbs supported - /// by the service, or empty if all verbs are supported. - /// - public string Verbs { get; set; } - - /// - /// Used to rank the precedences of route definitions in reverse routing. - /// i.e. Priorities below 0 are auto-generated have less precedence. - /// - public int Priority { get; set; } - - protected bool Equals(RouteAttribute other) - { - return base.Equals(other) - && string.Equals(Path, other.Path) - && string.Equals(Summary, other.Summary) - && string.Equals(Notes, other.Notes) - && string.Equals(Verbs, other.Verbs) - && Priority == other.Priority; - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - if (obj.GetType() != this.GetType()) - { - return false; - } - - return Equals((RouteAttribute)obj); - } - - public override int GetHashCode() - { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = (hashCode * 397) ^ (Path != null ? Path.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ (Summary != null ? Summary.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ (Notes != null ? Notes.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ (Verbs != null ? Verbs.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ Priority; - return hashCode; - } - } - } -} diff --git a/MediaBrowser.Model/Session/PlayRequest.cs b/MediaBrowser.Model/Session/PlayRequest.cs index eeb25c2e8..6a66465a2 100644 --- a/MediaBrowser.Model/Session/PlayRequest.cs +++ b/MediaBrowser.Model/Session/PlayRequest.cs @@ -2,7 +2,6 @@ #pragma warning disable CS1591 using System; -using MediaBrowser.Model.Services; namespace MediaBrowser.Model.Session { diff --git a/tests/Jellyfin.Server.Implementations.Tests/HttpServer/ResponseFilterTests.cs b/tests/Jellyfin.Server.Implementations.Tests/HttpServer/ResponseFilterTests.cs deleted file mode 100644 index 39bd94b59..000000000 --- a/tests/Jellyfin.Server.Implementations.Tests/HttpServer/ResponseFilterTests.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Emby.Server.Implementations.HttpServer; -using Xunit; - -namespace Jellyfin.Server.Implementations.Tests.HttpServer -{ - public class ResponseFilterTests - { - [Theory] - [InlineData(null, null)] - [InlineData("", "")] - [InlineData("This is a clean string.", "This is a clean string.")] - [InlineData("This isn't \n\ra clean string.", "This isn't a clean string.")] - public void RemoveControlCharacters_ValidArgs_Correct(string? input, string? result) - { - Assert.Equal(result, ResponseFilter.RemoveControlCharacters(input)); - } - } -} -- cgit v1.2.3 From 5813f8073c5bbe67a6071aed7084adc68f38fd48 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Thu, 3 Sep 2020 00:31:42 +0200 Subject: Move HttpListenerHost middleware up the pipeline --- Emby.Server.Implementations/ApplicationHost.cs | 2 +- .../HttpServer/HttpListenerHost.cs | 348 +++------------------ Jellyfin.Server/Startup.cs | 30 +- .../Extensions/HttpContextExtensions.cs | 2 +- MediaBrowser.Controller/Net/IHttpServer.cs | 7 +- 5 files changed, 78 insertions(+), 311 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 4f47d1999..5ed0ad415 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -501,7 +501,7 @@ namespace Emby.Server.Implementations } public Task ExecuteHttpHandlerAsync(HttpContext context, Func next) - => _httpServer.RequestHandler(context); + => _httpServer.RequestHandler(context, next); /// /// Registers services/resources with the service collection that will be available via DI. diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 4165cdb96..27369960b 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -2,26 +2,17 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.IO; using System.Linq; -using System.Net.Sockets; using System.Net.WebSockets; -using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Events; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Globalization; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; @@ -39,32 +30,25 @@ namespace Emby.Server.Implementations.HttpServer private readonly ILoggerFactory _loggerFactory; private readonly IServerConfigurationManager _config; private readonly INetworkManager _networkManager; - private readonly IServerApplicationHost _appHost; private readonly string _defaultRedirectPath; private readonly string _baseUrlPrefix; - private readonly IHostEnvironment _hostEnvironment; - private IWebSocketListener[] _webSocketListeners = Array.Empty(); private bool _disposed = false; public HttpListenerHost( - IServerApplicationHost applicationHost, ILogger logger, IServerConfigurationManager config, IConfiguration configuration, INetworkManager networkManager, ILocalizationManager localizationManager, - IHostEnvironment hostEnvironment, ILoggerFactory loggerFactory) { - _appHost = applicationHost; _logger = logger; _config = config; _defaultRedirectPath = configuration[DefaultRedirectKey]; _baseUrlPrefix = _config.Configuration.BaseUrl; _networkManager = networkManager; - _hostEnvironment = hostEnvironment; _loggerFactory = loggerFactory; Instance = this; @@ -79,122 +63,6 @@ namespace Emby.Server.Implementations.HttpServer public string GlobalResponse { get; set; } - private static Exception GetActualException(Exception ex) - { - if (ex is AggregateException agg) - { - var inner = agg.InnerException; - if (inner != null) - { - return GetActualException(inner); - } - else - { - var inners = agg.InnerExceptions; - if (inners.Count > 0) - { - return GetActualException(inners[0]); - } - } - } - - return ex; - } - - private int GetStatusCode(Exception ex) - { - switch (ex) - { - case ArgumentException _: return 400; - case AuthenticationException _: return 401; - case SecurityException _: return 403; - case DirectoryNotFoundException _: - case FileNotFoundException _: - case ResourceNotFoundException _: return 404; - case MethodNotAllowedException _: return 405; - default: return 500; - } - } - - private async Task ErrorHandler(Exception ex, HttpContext httpContext, int statusCode, string urlToLog, bool ignoreStackTrace) - { - if (ignoreStackTrace) - { - _logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog); - } - else - { - _logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog); - } - - var httpRes = httpContext.Response; - - if (httpRes.HasStarted) - { - return; - } - - httpRes.StatusCode = statusCode; - - var errContent = _hostEnvironment.IsDevelopment() - ? (NormalizeExceptionMessage(ex) ?? string.Empty) - : "Error processing request."; - httpRes.ContentType = "text/plain"; - httpRes.ContentLength = errContent.Length; - await httpRes.WriteAsync(errContent).ConfigureAwait(false); - } - - private string NormalizeExceptionMessage(Exception ex) - { - // Do not expose the exception message for AuthenticationException - if (ex is AuthenticationException) - { - return null; - } - - // Strip any information we don't want to reveal - return ex.Message - ?.Replace(_config.ApplicationPaths.ProgramSystemPath, string.Empty, StringComparison.OrdinalIgnoreCase) - .Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase); - } - - public static string RemoveQueryStringByKey(string url, string key) - { - var uri = new Uri(url); - - // this gets all the query string key value pairs as a collection - var newQueryString = QueryHelpers.ParseQuery(uri.Query); - - var originalCount = newQueryString.Count; - - if (originalCount == 0) - { - return url; - } - - // this removes the key if exists - newQueryString.Remove(key); - - if (originalCount == newQueryString.Count) - { - return url; - } - - // this gets the page path from root without QueryString - string pagePathWithoutQueryString = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0]; - - return newQueryString.Count > 0 - ? QueryHelpers.AddQueryString(pagePathWithoutQueryString, newQueryString.ToDictionary(kv => kv.Key, kv => kv.Value.ToString())) - : pagePathWithoutQueryString; - } - - private static string GetUrlToLog(string url) - { - url = RemoveQueryStringByKey(url, "api_key"); - - return url; - } - private static string NormalizeConfiguredLocalAddress(string address) { var add = address.AsSpan().Trim('/'); @@ -267,187 +135,90 @@ namespace Emby.Server.Implementations.HttpServer return true; } - /// - /// Validate a connection from a remote IP address to a URL to see if a redirection to HTTPS is required. - /// - /// True if the request is valid, or false if the request is not valid and an HTTPS redirect is required. - private bool ValidateSsl(string remoteIp, string urlString) - { - if (_config.Configuration.RequireHttps - && _appHost.ListenWithHttps - && !urlString.Contains("https://", StringComparison.OrdinalIgnoreCase)) - { - // 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 - || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1) - { - return true; - } - - if (!_networkManager.IsInLocalNetwork(remoteIp)) - { - return false; - } - } - - return true; - } - /// - public Task RequestHandler(HttpContext context) + public Task RequestHandler(HttpContext context, Func next) { if (context.WebSockets.IsWebSocketRequest) { return WebSocketRequestHandler(context); } - return RequestHandler(context, context.RequestAborted); + return HttpRequestHandler(context, next); } /// /// Overridable method that can be used to implement a custom handler. /// - private async Task RequestHandler(HttpContext httpContext, CancellationToken cancellationToken) + private async Task HttpRequestHandler(HttpContext httpContext, Func next) { - var stopWatch = new Stopwatch(); - stopWatch.Start(); + var cancellationToken = httpContext.RequestAborted; var httpRes = httpContext.Response; var host = httpContext.Request.Host.ToString(); var localPath = httpContext.Request.Path.ToString(); - var urlString = httpContext.Request.GetDisplayUrl(); - string urlToLog = GetUrlToLog(urlString); string remoteIp = httpContext.Request.RemoteIp(); - try + if (_disposed) { - if (_disposed) - { - httpRes.StatusCode = 503; - httpRes.ContentType = "text/plain"; - await httpRes.WriteAsync("Server shutting down", cancellationToken).ConfigureAwait(false); - return; - } - - if (!ValidateHost(host)) - { - httpRes.StatusCode = 400; - httpRes.ContentType = "text/plain"; - await httpRes.WriteAsync("Invalid host", cancellationToken).ConfigureAwait(false); - return; - } - - if (!ValidateRequest(remoteIp, httpContext.Request.IsLocal())) - { - httpRes.StatusCode = 403; - httpRes.ContentType = "text/plain"; - await httpRes.WriteAsync("Forbidden", cancellationToken).ConfigureAwait(false); - return; - } - - if (!ValidateSsl(httpContext.Request.RemoteIp(), urlString)) - { - RedirectToSecureUrl(httpRes, urlString); - return; - } - - if (string.Equals(httpContext.Request.Method, "OPTIONS", StringComparison.OrdinalIgnoreCase)) - { - httpRes.StatusCode = 200; - foreach (var (key, value) in GetDefaultCorsHeaders(httpContext)) - { - httpRes.Headers.Add(key, value); - } - - httpRes.ContentType = "text/plain"; - await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false); - return; - } - - if (string.Equals(localPath, _baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) - || string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase) - || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase) - || string.IsNullOrEmpty(localPath) - || !localPath.StartsWith(_baseUrlPrefix, StringComparison.OrdinalIgnoreCase)) - { - // Always redirect back to the default path if the base prefix is invalid or missing - _logger.LogDebug("Normalizing a URL at {0}", localPath); - httpRes.Redirect(_baseUrlPrefix + "/" + _defaultRedirectPath); - return; - } + httpRes.StatusCode = 503; + httpRes.ContentType = "text/plain"; + await httpRes.WriteAsync("Server shutting down", cancellationToken).ConfigureAwait(false); + return; + } - if (!string.IsNullOrEmpty(GlobalResponse)) - { - // We don't want the address pings in ApplicationHost to fail - if (localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1) - { - httpRes.StatusCode = 503; - httpRes.ContentType = "text/html"; - await httpRes.WriteAsync(GlobalResponse, cancellationToken).ConfigureAwait(false); - return; - } - } + if (!ValidateHost(host)) + { + httpRes.StatusCode = 400; + httpRes.ContentType = "text/plain"; + await httpRes.WriteAsync("Invalid host", cancellationToken).ConfigureAwait(false); + return; + } - throw new FileNotFoundException(); + if (!ValidateRequest(remoteIp, httpContext.Request.IsLocal())) + { + httpRes.StatusCode = 403; + httpRes.ContentType = "text/plain"; + await httpRes.WriteAsync("Forbidden", cancellationToken).ConfigureAwait(false); + return; } - catch (Exception requestEx) + + if (string.Equals(httpContext.Request.Method, "OPTIONS", StringComparison.OrdinalIgnoreCase)) { - try + httpRes.StatusCode = 200; + foreach (var (key, value) in GetDefaultCorsHeaders(httpContext)) { - var requestInnerEx = GetActualException(requestEx); - var statusCode = GetStatusCode(requestInnerEx); - - foreach (var (key, value) in GetDefaultCorsHeaders(httpContext)) - { - if (!httpRes.Headers.ContainsKey(key)) - { - httpRes.Headers.Add(key, value); - } - } - - bool ignoreStackTrace = - requestInnerEx is SocketException - || requestInnerEx is IOException - || requestInnerEx is OperationCanceledException - || requestInnerEx is SecurityException - || requestInnerEx is AuthenticationException - || requestInnerEx is FileNotFoundException; - - // Do not handle 500 server exceptions manually when in development mode. - // Instead, re-throw the exception so it can be handled by the DeveloperExceptionPageMiddleware. - // However, do not use the DeveloperExceptionPageMiddleware when the stack trace should be ignored, - // because it will log the stack trace when it handles the exception. - if (statusCode == 500 && !ignoreStackTrace && _hostEnvironment.IsDevelopment()) - { - throw; - } - - await ErrorHandler(requestInnerEx, httpContext, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false); + httpRes.Headers.Add(key, value); } - catch (Exception handlerException) - { - var aggregateEx = new AggregateException("Error while handling request exception", requestEx, handlerException); - _logger.LogError(aggregateEx, "Error while handling exception in response to {Url}", urlToLog); - if (_hostEnvironment.IsDevelopment()) - { - throw aggregateEx; - } - } + httpRes.ContentType = "text/plain"; + await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false); + return; } - finally + + if (string.Equals(localPath, _baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) + || string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase) + || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase) + || string.IsNullOrEmpty(localPath) + || !localPath.StartsWith(_baseUrlPrefix, StringComparison.OrdinalIgnoreCase)) { - if (httpRes.StatusCode >= 500) - { - _logger.LogDebug("Sending HTTP Response 500 in response to {Url}", urlToLog); - } + // Always redirect back to the default path if the base prefix is invalid or missing + _logger.LogDebug("Normalizing a URL at {0}", localPath); + httpRes.Redirect(_baseUrlPrefix + "/" + _defaultRedirectPath); + return; + } - stopWatch.Stop(); - var elapsed = stopWatch.Elapsed; - if (elapsed.TotalMilliseconds > 500) + if (!string.IsNullOrEmpty(GlobalResponse)) + { + // We don't want the address pings in ApplicationHost to fail + if (localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1) { - _logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog); + httpRes.StatusCode = 503; + httpRes.ContentType = "text/html"; + await httpRes.WriteAsync(GlobalResponse, cancellationToken).ConfigureAwait(false); + return; } } + + await next().ConfigureAwait(false); } private async Task WebSocketRequestHandler(HttpContext context) @@ -508,21 +279,6 @@ namespace Emby.Server.Implementations.HttpServer return headers; } - private void RedirectToSecureUrl(HttpResponse httpRes, string url) - { - if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) - { - var builder = new UriBuilder(uri) - { - Port = _config.Configuration.PublicHttpsPort, - Scheme = "https" - }; - url = builder.Uri.ToString(); - } - - httpRes.Redirect(url); - } - /// /// Adds the rest handlers. /// diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index cbc1c040c..81243902a 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -23,17 +23,19 @@ namespace Jellyfin.Server public class Startup { private readonly IServerConfigurationManager _serverConfigurationManager; - private readonly IApplicationHost _applicationHost; + private readonly IServerApplicationHost _serverApplicationHost; /// /// Initializes a new instance of the class. /// /// The server configuration manager. - /// The application host. - public Startup(IServerConfigurationManager serverConfigurationManager, IApplicationHost applicationHost) + /// The server application host. + public Startup( + IServerConfigurationManager serverConfigurationManager, + IServerApplicationHost serverApplicationHost) { _serverConfigurationManager = serverConfigurationManager; - _applicationHost = applicationHost; + _serverApplicationHost = serverApplicationHost; } /// @@ -44,7 +46,9 @@ namespace Jellyfin.Server { services.AddResponseCompression(); services.AddHttpContextAccessor(); - services.AddJellyfinApi(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/'), _applicationHost.GetApiPluginAssemblies()); + services.AddJellyfinApi( + _serverConfigurationManager.Configuration.BaseUrl.TrimStart('/'), + _serverApplicationHost.GetApiPluginAssemblies()); services.AddJellyfinApiSwagger(); @@ -53,7 +57,9 @@ namespace Jellyfin.Server services.AddJellyfinApiAuthorization(); - var productHeader = new ProductInfoHeaderValue(_applicationHost.Name.Replace(' ', '-'), _applicationHost.ApplicationVersionString); + var productHeader = new ProductInfoHeaderValue( + _serverApplicationHost.Name.Replace(' ', '-'), + _serverApplicationHost.ApplicationVersionString); services .AddHttpClient(NamedClient.Default, c => { @@ -64,7 +70,7 @@ namespace Jellyfin.Server services.AddHttpClient(NamedClient.MusicBrainz, c => { c.DefaultRequestHeaders.UserAgent.Add(productHeader); - c.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue($"({_applicationHost.ApplicationUserAgentAddress})")); + c.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue($"({_serverApplicationHost.ApplicationUserAgentAddress})")); }) .ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler()); } @@ -93,7 +99,11 @@ namespace Jellyfin.Server app.UseResponseCompression(); - // TODO app.UseMiddleware(); + if (_serverConfigurationManager.Configuration.RequireHttps + && _serverApplicationHost.ListenWithHttps) + { + app.UseHttpsRedirection(); + } app.UseAuthentication(); app.UseJellyfinApiSwagger(_serverConfigurationManager); @@ -106,6 +116,8 @@ namespace Jellyfin.Server app.UseHttpMetrics(); } + app.Use(serverApplicationHost.ExecuteHttpHandlerAsync); + app.UseEndpoints(endpoints => { endpoints.MapControllers(); @@ -115,8 +127,6 @@ namespace Jellyfin.Server } }); - app.Use(serverApplicationHost.ExecuteHttpHandlerAsync); - // Add type descriptor for legacy datetime parsing. TypeDescriptor.AddAttributes(typeof(DateTime?), new TypeConverterAttribute(typeof(DateTimeTypeConverter))); } diff --git a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs index 86c3b3536..e0cf3f9ac 100644 --- a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs +++ b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs @@ -28,7 +28,7 @@ namespace MediaBrowser.Common.Extensions /// The remote caller IP address. public static string RemoteIp(this HttpRequest request) { - var cachedRemoteIp = request.HttpContext.Items["RemoteIp"].ToString(); + var cachedRemoteIp = request.HttpContext.Items["RemoteIp"]?.ToString(); if (!string.IsNullOrEmpty(cachedRemoteIp)) { return cachedRemoteIp; diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs index 637dd2be3..6739f2fa6 100644 --- a/MediaBrowser.Controller/Net/IHttpServer.cs +++ b/MediaBrowser.Controller/Net/IHttpServer.cs @@ -35,9 +35,10 @@ namespace MediaBrowser.Controller.Net /// /// The HTTP request handler. /// - /// - /// - Task RequestHandler(HttpContext context); + /// The current HTTP context. + /// The next middleware in the ASP.NET pipeline. + /// The task. + Task RequestHandler(HttpContext context, Func next); /// /// Get the default CORS headers. -- cgit v1.2.3 From 571d0570f5560bde79d21c33173742f6a31e24cf Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Thu, 3 Sep 2020 11:32:22 +0200 Subject: Kill HttpListenerHost --- Emby.Server.Implementations/ApplicationHost.cs | 19 +- .../ConfigurationOptions.cs | 2 +- .../HttpServer/HttpListenerHost.cs | 315 --------------------- .../HttpServer/WebSocketManager.cs | 102 +++++++ .../Session/SessionWebSocketListener.cs | 12 +- .../Extensions/ApiApplicationBuilderExtensions.cs | 61 ++++ .../Middleware/BaseUrlRedirectionMiddleware.cs | 62 ++++ .../Middleware/CorsOptionsResponseMiddleware.cs | 69 +++++ .../IpBasedAccessValidationMiddleware.cs | 76 +++++ .../Middleware/LanFilteringMiddleware.cs | 76 +++++ .../Middleware/ServerStartupMessageMiddleware.cs | 38 +++ .../Middleware/WebSocketHandlerMiddleware.cs | 40 +++ Jellyfin.Server/Program.cs | 4 +- Jellyfin.Server/Startup.cs | 12 +- .../Extensions/ConfigurationExtensions.cs | 6 + MediaBrowser.Controller/IServerApplicationHost.cs | 3 +- MediaBrowser.Controller/Net/IHttpServer.cs | 50 ---- MediaBrowser.Controller/Net/IWebSocketManager.cs | 32 +++ 18 files changed, 589 insertions(+), 390 deletions(-) delete mode 100644 Emby.Server.Implementations/HttpServer/HttpListenerHost.cs create mode 100644 Emby.Server.Implementations/HttpServer/WebSocketManager.cs create mode 100644 Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs create mode 100644 Jellyfin.Server/Middleware/CorsOptionsResponseMiddleware.cs create mode 100644 Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs create mode 100644 Jellyfin.Server/Middleware/LanFilteringMiddleware.cs create mode 100644 Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs create mode 100644 Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs delete mode 100644 MediaBrowser.Controller/Net/IHttpServer.cs create mode 100644 MediaBrowser.Controller/Net/IWebSocketManager.cs (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 5ed0ad415..c8af6b73a 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -96,12 +96,12 @@ using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.TheTvdb; using MediaBrowser.Providers.Subtitles; using MediaBrowser.XbmcMetadata.Providers; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Prometheus.DotNetRuntime; using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; +using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager; namespace Emby.Server.Implementations { @@ -122,9 +122,11 @@ namespace Emby.Server.Implementations private IMediaEncoder _mediaEncoder; private ISessionManager _sessionManager; - private IHttpServer _httpServer; + private IWebSocketManager _webSocketManager; private IHttpClient _httpClient; + private string[] _urlPrefixes; + /// /// Gets a value indicating whether this instance can self restart. /// @@ -444,7 +446,6 @@ namespace Emby.Server.Implementations Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed); Logger.LogInformation("Core startup complete"); - _httpServer.GlobalResponse = null; stopWatch.Restart(); await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false); @@ -500,9 +501,6 @@ namespace Emby.Server.Implementations RegisterServices(); } - public Task ExecuteHttpHandlerAsync(HttpContext context, Func next) - => _httpServer.RequestHandler(context, next); - /// /// Registers services/resources with the service collection that will be available via DI. /// @@ -577,7 +575,7 @@ namespace Emby.Server.Implementations ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); ServiceCollection.AddSingleton(); @@ -650,7 +648,7 @@ namespace Emby.Server.Implementations _mediaEncoder = Resolve(); _sessionManager = Resolve(); - _httpServer = Resolve(); + _webSocketManager = Resolve(); _httpClient = Resolve(); ((AuthenticationRepository)Resolve()).Initialize(); @@ -771,7 +769,8 @@ namespace Emby.Server.Implementations .Where(i => i != null) .ToArray(); - _httpServer.Init(GetExports(), GetUrlPrefixes()); + _urlPrefixes = GetUrlPrefixes().ToArray(); + _webSocketManager.Init(GetExports()); Resolve().AddParts( GetExports(), @@ -937,7 +936,7 @@ namespace Emby.Server.Implementations } } - if (!_httpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase)) + if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase)) { requiresRestart = true; } diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index 64ccff53b..fde6fa115 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -15,7 +15,7 @@ namespace Emby.Server.Implementations public static Dictionary DefaultConfiguration => new Dictionary { { HostWebClientKey, bool.TrueString }, - { HttpListenerHost.DefaultRedirectKey, "web/index.html" }, + { DefaultRedirectKey, "web/index.html" }, { FfmpegProbeSizeKey, "1G" }, { FfmpegAnalyzeDurationKey, "200M" }, { PlaylistsAllowDuplicatesKey, bool.TrueString }, diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs deleted file mode 100644 index 27369960b..000000000 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ /dev/null @@ -1,315 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.WebSockets; -using System.Threading.Tasks; -using Jellyfin.Data.Events; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Globalization; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; - -namespace Emby.Server.Implementations.HttpServer -{ - public class HttpListenerHost : IHttpServer - { - /// - /// The key for a setting that specifies the default redirect path - /// to use for requests where the URL base prefix is invalid or missing. - /// - public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath"; - - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly IServerConfigurationManager _config; - private readonly INetworkManager _networkManager; - private readonly string _defaultRedirectPath; - private readonly string _baseUrlPrefix; - - private IWebSocketListener[] _webSocketListeners = Array.Empty(); - private bool _disposed = false; - - public HttpListenerHost( - ILogger logger, - IServerConfigurationManager config, - IConfiguration configuration, - INetworkManager networkManager, - ILocalizationManager localizationManager, - ILoggerFactory loggerFactory) - { - _logger = logger; - _config = config; - _defaultRedirectPath = configuration[DefaultRedirectKey]; - _baseUrlPrefix = _config.Configuration.BaseUrl; - _networkManager = networkManager; - _loggerFactory = loggerFactory; - - Instance = this; - GlobalResponse = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); - } - - public event EventHandler> WebSocketConnected; - - public static HttpListenerHost Instance { get; protected set; } - - public string[] UrlPrefixes { get; private set; } - - public string GlobalResponse { get; set; } - - private static string NormalizeConfiguredLocalAddress(string address) - { - var add = address.AsSpan().Trim('/'); - int index = add.IndexOf('/'); - if (index != -1) - { - add = add.Slice(index + 1); - } - - return add.TrimStart('/').ToString(); - } - - private bool ValidateHost(string host) - { - var hosts = _config - .Configuration - .LocalNetworkAddresses - .Select(NormalizeConfiguredLocalAddress) - .ToList(); - - if (hosts.Count == 0) - { - return true; - } - - host ??= string.Empty; - - if (_networkManager.IsInPrivateAddressSpace(host)) - { - hosts.Add("localhost"); - hosts.Add("127.0.0.1"); - - return hosts.Any(i => host.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1); - } - - return true; - } - - private bool ValidateRequest(string remoteIp, bool isLocal) - { - if (isLocal) - { - return true; - } - - if (_config.Configuration.EnableRemoteAccess) - { - var addressFilter = _config.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); - - if (addressFilter.Length > 0 && !_networkManager.IsInLocalNetwork(remoteIp)) - { - if (_config.Configuration.IsRemoteIPFilterBlacklist) - { - return !_networkManager.IsAddressInSubnets(remoteIp, addressFilter); - } - else - { - return _networkManager.IsAddressInSubnets(remoteIp, addressFilter); - } - } - } - else - { - if (!_networkManager.IsInLocalNetwork(remoteIp)) - { - return false; - } - } - - return true; - } - - /// - public Task RequestHandler(HttpContext context, Func next) - { - if (context.WebSockets.IsWebSocketRequest) - { - return WebSocketRequestHandler(context); - } - - return HttpRequestHandler(context, next); - } - - /// - /// Overridable method that can be used to implement a custom handler. - /// - private async Task HttpRequestHandler(HttpContext httpContext, Func next) - { - var cancellationToken = httpContext.RequestAborted; - var httpRes = httpContext.Response; - var host = httpContext.Request.Host.ToString(); - var localPath = httpContext.Request.Path.ToString(); - string remoteIp = httpContext.Request.RemoteIp(); - - if (_disposed) - { - httpRes.StatusCode = 503; - httpRes.ContentType = "text/plain"; - await httpRes.WriteAsync("Server shutting down", cancellationToken).ConfigureAwait(false); - return; - } - - if (!ValidateHost(host)) - { - httpRes.StatusCode = 400; - httpRes.ContentType = "text/plain"; - await httpRes.WriteAsync("Invalid host", cancellationToken).ConfigureAwait(false); - return; - } - - if (!ValidateRequest(remoteIp, httpContext.Request.IsLocal())) - { - httpRes.StatusCode = 403; - httpRes.ContentType = "text/plain"; - await httpRes.WriteAsync("Forbidden", cancellationToken).ConfigureAwait(false); - return; - } - - if (string.Equals(httpContext.Request.Method, "OPTIONS", StringComparison.OrdinalIgnoreCase)) - { - httpRes.StatusCode = 200; - foreach (var (key, value) in GetDefaultCorsHeaders(httpContext)) - { - httpRes.Headers.Add(key, value); - } - - httpRes.ContentType = "text/plain"; - await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false); - return; - } - - if (string.Equals(localPath, _baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) - || string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase) - || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase) - || string.IsNullOrEmpty(localPath) - || !localPath.StartsWith(_baseUrlPrefix, StringComparison.OrdinalIgnoreCase)) - { - // Always redirect back to the default path if the base prefix is invalid or missing - _logger.LogDebug("Normalizing a URL at {0}", localPath); - httpRes.Redirect(_baseUrlPrefix + "/" + _defaultRedirectPath); - return; - } - - if (!string.IsNullOrEmpty(GlobalResponse)) - { - // We don't want the address pings in ApplicationHost to fail - if (localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1) - { - httpRes.StatusCode = 503; - httpRes.ContentType = "text/html"; - await httpRes.WriteAsync(GlobalResponse, cancellationToken).ConfigureAwait(false); - return; - } - } - - await next().ConfigureAwait(false); - } - - 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); - - using var connection = new WebSocketConnection( - _loggerFactory.CreateLogger(), - webSocket, - context.Connection.RemoteIpAddress, - context.Request.Query) - { - OnReceive = ProcessWebSocketMessageReceived - }; - - WebSocketConnected?.Invoke(this, new GenericEventArgs(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; - } - } - } - - /// - public IDictionary GetDefaultCorsHeaders(HttpContext httpContext) - { - var origin = httpContext.Request.Headers["Origin"]; - if (origin == StringValues.Empty) - { - origin = httpContext.Request.Headers["Host"]; - if (origin == StringValues.Empty) - { - origin = "*"; - } - } - - var headers = new Dictionary(); - headers.Add("Access-Control-Allow-Origin", origin); - headers.Add("Access-Control-Allow-Credentials", "true"); - headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); - headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization, Cookie"); - return headers; - } - - /// - /// Adds the rest handlers. - /// - /// The web socket listeners. - /// The URL prefixes. See . - public void Init(IEnumerable listeners, IEnumerable urlPrefixes) - { - _webSocketListeners = listeners.ToArray(); - UrlPrefixes = urlPrefixes.ToArray(); - } - - /// - /// Processes the web socket message received. - /// - /// The result. - private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result) - { - if (_disposed) - { - return Task.CompletedTask; - } - - IEnumerable GetTasks() - { - foreach (var x in _webSocketListeners) - { - yield return x.ProcessMessageAsync(result); - } - } - - return Task.WhenAll(GetTasks()); - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs new file mode 100644 index 000000000..89c1b7ea0 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs @@ -0,0 +1,102 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Threading.Tasks; +using Jellyfin.Data.Events; +using MediaBrowser.Controller.Net; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.HttpServer +{ + public class WebSocketManager : IWebSocketManager + { + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + + private IWebSocketListener[] _webSocketListeners = Array.Empty(); + private bool _disposed = false; + + public WebSocketManager( + ILogger logger, + ILoggerFactory loggerFactory) + { + _logger = logger; + _loggerFactory = loggerFactory; + } + + public event EventHandler> WebSocketConnected; + + /// + public 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); + + using var connection = new WebSocketConnection( + _loggerFactory.CreateLogger(), + webSocket, + context.Connection.RemoteIpAddress, + context.Request.Query) + { + OnReceive = ProcessWebSocketMessageReceived + }; + + WebSocketConnected?.Invoke(this, new GenericEventArgs(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; + } + } + } + + /// + /// Adds the rest handlers. + /// + /// The web socket listeners. + public void Init(IEnumerable listeners) + { + _webSocketListeners = listeners.ToArray(); + } + + /// + /// Processes the web socket message received. + /// + /// The result. + private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result) + { + if (_disposed) + { + return Task.CompletedTask; + } + + IEnumerable GetTasks() + { + foreach (var x in _webSocketListeners) + { + yield return x.ProcessMessageAsync(result); + } + } + + return Task.WhenAll(GetTasks()); + } + } +} diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index 1da7a6473..15c2af220 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.Session private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; - private readonly IHttpServer _httpServer; + private readonly IWebSocketManager _webSocketManager; /// /// The KeepAlive cancellation token. @@ -72,19 +72,19 @@ namespace Emby.Server.Implementations.Session /// The logger. /// The session manager. /// The logger factory. - /// The HTTP server. + /// The HTTP server. public SessionWebSocketListener( ILogger logger, ISessionManager sessionManager, ILoggerFactory loggerFactory, - IHttpServer httpServer) + IWebSocketManager webSocketManager) { _logger = logger; _sessionManager = sessionManager; _loggerFactory = loggerFactory; - _httpServer = httpServer; + _webSocketManager = webSocketManager; - httpServer.WebSocketConnected += OnServerManagerWebSocketConnected; + webSocketManager.WebSocketConnected += OnServerManagerWebSocketConnected; } private async void OnServerManagerWebSocketConnected(object sender, GenericEventArgs e) @@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.Session /// public void Dispose() { - _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected; + _webSocketManager.WebSocketConnected -= OnServerManagerWebSocketConnected; StopKeepAlive(); } diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index 745567703..33a8d7532 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -1,3 +1,4 @@ +using Jellyfin.Server.Middleware; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Builder; @@ -46,5 +47,65 @@ namespace Jellyfin.Server.Extensions c.RoutePrefix = $"{baseUrl}api-docs/redoc"; }); } + + /// + /// Adds IP based access validation to the application pipeline. + /// + /// The application builder. + /// The updated application builder. + public static IApplicationBuilder UseIpBasedAccessValidation(this IApplicationBuilder appBuilder) + { + return appBuilder.UseMiddleware(); + } + + /// + /// Adds LAN based access filtering to the application pipeline. + /// + /// The application builder. + /// The updated application builder. + public static IApplicationBuilder UseLanFiltering(this IApplicationBuilder appBuilder) + { + return appBuilder.UseMiddleware(); + } + + /// + /// Adds CORS OPTIONS request handling to the application pipeline. + /// + /// The application builder. + /// The updated application builder. + public static IApplicationBuilder UseCorsOptionsResponse(this IApplicationBuilder appBuilder) + { + return appBuilder.UseMiddleware(); + } + + /// + /// Adds base url redirection to the application pipeline. + /// + /// The application builder. + /// The updated application builder. + public static IApplicationBuilder UseBaseUrlRedirection(this IApplicationBuilder appBuilder) + { + return appBuilder.UseMiddleware(); + } + + /// + /// Adds a custom message during server startup to the application pipeline. + /// + /// The application builder. + /// The updated application builder. + public static IApplicationBuilder UseServerStartupMessage(this IApplicationBuilder appBuilder) + { + return appBuilder.UseMiddleware(); + } + + /// + /// Adds a WebSocket request handler to the application pipeline. + /// + /// The application builder. + /// The updated application builder. + public static IApplicationBuilder UseWebSocketHandler(this IApplicationBuilder appBuilder) + { + return appBuilder.UseMiddleware(); + } } } diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs new file mode 100644 index 000000000..9316737bd --- /dev/null +++ b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs @@ -0,0 +1,62 @@ +using System; +using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using ConfigurationExtensions = MediaBrowser.Controller.Extensions.ConfigurationExtensions; + +namespace Jellyfin.Server.Middleware +{ + /// + /// Redirect requests without baseurl prefix to the baseurl prefixed URL. + /// + public class BaseUrlRedirectionMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The next delegate in the pipeline. + /// The logger. + /// The application configuration. + public BaseUrlRedirectionMiddleware( + RequestDelegate next, + ILogger logger, + IConfiguration configuration) + { + _next = next; + _logger = logger; + _configuration = configuration; + } + + /// + /// Executes the middleware action. + /// + /// The current HTTP context. + /// The server configuration manager. + /// The async task. + public async Task Invoke(HttpContext httpContext, IServerConfigurationManager serverConfigurationManager) + { + var localPath = httpContext.Request.Path.ToString(); + var baseUrlPrefix = serverConfigurationManager.Configuration.BaseUrl; + + if (string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) + || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase) + || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase) + || string.IsNullOrEmpty(localPath) + || !localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase)) + { + // Always redirect back to the default path if the base prefix is invalid or missing + _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath); + httpContext.Response.Redirect(baseUrlPrefix + "/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]); + return; + } + + await _next(httpContext).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server/Middleware/CorsOptionsResponseMiddleware.cs b/Jellyfin.Server/Middleware/CorsOptionsResponseMiddleware.cs new file mode 100644 index 000000000..8214f8907 --- /dev/null +++ b/Jellyfin.Server/Middleware/CorsOptionsResponseMiddleware.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Net.Mime; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; + +namespace Jellyfin.Server.Middleware +{ + /// + /// Middleware for handling OPTIONS requests. + /// + public class CorsOptionsResponseMiddleware + { + private readonly RequestDelegate _next; + + /// + /// Initializes a new instance of the class. + /// + /// The next delegate in the pipeline. + public CorsOptionsResponseMiddleware(RequestDelegate next) + { + _next = next; + } + + /// + /// Executes the middleware action. + /// + /// The current HTTP context. + /// The async task. + public async Task Invoke(HttpContext httpContext) + { + if (string.Equals(httpContext.Request.Method, HttpMethods.Options, StringComparison.OrdinalIgnoreCase)) + { + httpContext.Response.StatusCode = 200; + foreach (var (key, value) in GetDefaultCorsHeaders(httpContext)) + { + httpContext.Response.Headers.Add(key, value); + } + + httpContext.Response.ContentType = MediaTypeNames.Text.Plain; + await httpContext.Response.WriteAsync(string.Empty, httpContext.RequestAborted).ConfigureAwait(false); + return; + } + + await _next(httpContext).ConfigureAwait(false); + } + + private static IDictionary GetDefaultCorsHeaders(HttpContext httpContext) + { + var origin = httpContext.Request.Headers["Origin"]; + if (origin == StringValues.Empty) + { + origin = httpContext.Request.Headers["Host"]; + if (origin == StringValues.Empty) + { + origin = "*"; + } + } + + var headers = new Dictionary(); + headers.Add("Access-Control-Allow-Origin", origin); + headers.Add("Access-Control-Allow-Credentials", "true"); + headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); + headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization, Cookie"); + return headers; + } + } +} diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs new file mode 100644 index 000000000..59b5fb1ed --- /dev/null +++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs @@ -0,0 +1,76 @@ +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Server.Middleware +{ + /// + /// Validates the IP of requests coming from local networks wrt. remote access. + /// + public class IpBasedAccessValidationMiddleware + { + private readonly RequestDelegate _next; + + /// + /// Initializes a new instance of the class. + /// + /// The next delegate in the pipeline. + public IpBasedAccessValidationMiddleware(RequestDelegate next) + { + _next = next; + } + + /// + /// Executes the middleware action. + /// + /// The current HTTP context. + /// The network manager. + /// The server configuration manager. + /// The async task. + public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager) + { + if (httpContext.Request.IsLocal()) + { + await _next(httpContext).ConfigureAwait(false); + return; + } + + var remoteIp = httpContext.Request.RemoteIp(); + + if (serverConfigurationManager.Configuration.EnableRemoteAccess) + { + var addressFilter = serverConfigurationManager.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); + + if (addressFilter.Length > 0 && !networkManager.IsInLocalNetwork(remoteIp)) + { + if (serverConfigurationManager.Configuration.IsRemoteIPFilterBlacklist) + { + if (networkManager.IsAddressInSubnets(remoteIp, addressFilter)) + { + return; + } + } + else + { + if (!networkManager.IsAddressInSubnets(remoteIp, addressFilter)) + { + return; + } + } + } + } + else + { + if (!networkManager.IsInLocalNetwork(remoteIp)) + { + return; + } + } + + await _next(httpContext).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs new file mode 100644 index 000000000..9d795145a --- /dev/null +++ b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs @@ -0,0 +1,76 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Server.Middleware +{ + /// + /// Validates the LAN host IP based on application configuration. + /// + public class LanFilteringMiddleware + { + private readonly RequestDelegate _next; + + /// + /// Initializes a new instance of the class. + /// + /// The next delegate in the pipeline. + public LanFilteringMiddleware(RequestDelegate next) + { + _next = next; + } + + /// + /// Executes the middleware action. + /// + /// The current HTTP context. + /// The network manager. + /// The server configuration manager. + /// The async task. + public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager) + { + var currentHost = httpContext.Request.Host.ToString(); + var hosts = serverConfigurationManager + .Configuration + .LocalNetworkAddresses + .Select(NormalizeConfiguredLocalAddress) + .ToList(); + + if (hosts.Count == 0) + { + await _next(httpContext).ConfigureAwait(false); + return; + } + + currentHost ??= string.Empty; + + if (networkManager.IsInPrivateAddressSpace(currentHost)) + { + hosts.Add("localhost"); + hosts.Add("127.0.0.1"); + + if (hosts.All(i => currentHost.IndexOf(i, StringComparison.OrdinalIgnoreCase) == -1)) + { + return; + } + } + + await _next(httpContext).ConfigureAwait(false); + } + + private static string NormalizeConfiguredLocalAddress(string address) + { + var add = address.AsSpan().Trim('/'); + int index = add.IndexOf('/'); + if (index != -1) + { + add = add.Slice(index + 1); + } + + return add.TrimStart('/').ToString(); + } + } +} diff --git a/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs b/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs new file mode 100644 index 000000000..4f347d6d3 --- /dev/null +++ b/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs @@ -0,0 +1,38 @@ +using System.Net.Mime; +using System.Threading.Tasks; +using MediaBrowser.Model.Globalization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Server.Middleware +{ + /// + /// Shows a custom message during server startup. + /// + public class ServerStartupMessageMiddleware + { + private readonly RequestDelegate _next; + + /// + /// Initializes a new instance of the class. + /// + /// The next delegate in the pipeline. + public ServerStartupMessageMiddleware(RequestDelegate next) + { + _next = next; + } + + /// + /// Executes the middleware action. + /// + /// The current HTTP context. + /// The localization manager. + /// The async task. + public async Task Invoke(HttpContext httpContext, ILocalizationManager localizationManager) + { + var message = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); + httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; + httpContext.Response.ContentType = MediaTypeNames.Text.Html; + await httpContext.Response.WriteAsync(message, httpContext.RequestAborted).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs b/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs new file mode 100644 index 000000000..b7a5d2b34 --- /dev/null +++ b/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs @@ -0,0 +1,40 @@ +using System.Threading.Tasks; +using MediaBrowser.Controller.Net; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Server.Middleware +{ + /// + /// Handles WebSocket requests. + /// + public class WebSocketHandlerMiddleware + { + private readonly RequestDelegate _next; + + /// + /// Initializes a new instance of the class. + /// + /// The next delegate in the pipeline. + public WebSocketHandlerMiddleware(RequestDelegate next) + { + _next = next; + } + + /// + /// Executes the middleware action. + /// + /// The current HTTP context. + /// The WebSocket connection manager. + /// The async task. + public async Task Invoke(HttpContext httpContext, IWebSocketManager webSocketManager) + { + if (!httpContext.WebSockets.IsWebSocketRequest) + { + await _next(httpContext).ConfigureAwait(false); + return; + } + + await webSocketManager.WebSocketRequestHandler(httpContext).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 14cc5f4c2..b9a90f9db 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -11,7 +11,6 @@ using System.Threading; using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; -using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Networking; using Jellyfin.Api.Controllers; @@ -28,6 +27,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Serilog; using Serilog.Extensions.Logging; using SQLitePCL; +using ConfigurationExtensions = MediaBrowser.Controller.Extensions.ConfigurationExtensions; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Jellyfin.Server @@ -594,7 +594,7 @@ namespace Jellyfin.Server var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration; if (startupConfig != null && !startupConfig.HostWebClient()) { - inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "api-docs/swagger"; + inMemoryDefaultConfig[ConfigurationExtensions.DefaultRedirectKey] = "api-docs/swagger"; } return config diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 9316ab79e..80f679420 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -84,11 +84,9 @@ namespace Jellyfin.Server /// /// The application builder. /// The webhost environment. - /// The server application host. public void Configure( IApplicationBuilder app, - IWebHostEnvironment env, - IServerApplicationHost serverApplicationHost) + IWebHostEnvironment env) { if (env.IsDevelopment()) { @@ -120,7 +118,11 @@ namespace Jellyfin.Server app.UseHttpMetrics(); } - app.Use(serverApplicationHost.ExecuteHttpHandlerAsync); + app.UseLanFiltering(); + app.UseIpBasedAccessValidation(); + app.UseCorsOptionsResponse(); + app.UseBaseUrlRedirection(); + app.UseWebSocketHandler(); app.UseEndpoints(endpoints => { @@ -131,6 +133,8 @@ namespace Jellyfin.Server } }); + app.UseServerStartupMessage(); + // Add type descriptor for legacy datetime parsing. TypeDescriptor.AddAttributes(typeof(DateTime?), new TypeConverterAttribute(typeof(DateTimeTypeConverter))); } diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs index 4c2209b67..f9285c768 100644 --- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs +++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs @@ -8,6 +8,12 @@ namespace MediaBrowser.Controller.Extensions /// public static class ConfigurationExtensions { + /// + /// The key for a setting that specifies the default redirect path + /// to use for requests where the URL base prefix is invalid or missing.. + /// + public const string DefaultRedirectKey = "DefaultRedirectPath"; + /// /// The key for a setting that indicates whether the application should host web client content. /// diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 39b896c0f..d482c19d9 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -117,8 +117,7 @@ namespace MediaBrowser.Controller IEnumerable GetWakeOnLanInfo(); string ExpandVirtualPath(string path); - string ReverseVirtualPath(string path); - Task ExecuteHttpHandlerAsync(HttpContext context, Func next); + string ReverseVirtualPath(string path); } } diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs deleted file mode 100644 index 6739f2fa6..000000000 --- a/MediaBrowser.Controller/Net/IHttpServer.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Jellyfin.Data.Events; -using Microsoft.AspNetCore.Http; - -namespace MediaBrowser.Controller.Net -{ - /// - /// Interface IHttpServer. - /// - public interface IHttpServer - { - /// - /// Gets the URL prefix. - /// - /// The URL prefix. - string[] UrlPrefixes { get; } - - /// - /// Occurs when [web socket connected]. - /// - event EventHandler> WebSocketConnected; - - /// - /// Inits this instance. - /// - void Init(IEnumerable listener, IEnumerable urlPrefixes); - - /// - /// If set, all requests will respond with this message. - /// - string GlobalResponse { get; set; } - - /// - /// The HTTP request handler. - /// - /// The current HTTP context. - /// The next middleware in the ASP.NET pipeline. - /// The task. - Task RequestHandler(HttpContext context, Func next); - - /// - /// Get the default CORS headers. - /// - /// The HTTP context of the current request. - /// The default CORS headers for the context. - IDictionary GetDefaultCorsHeaders(HttpContext httpContext); - } -} diff --git a/MediaBrowser.Controller/Net/IWebSocketManager.cs b/MediaBrowser.Controller/Net/IWebSocketManager.cs new file mode 100644 index 000000000..e9f00ae88 --- /dev/null +++ b/MediaBrowser.Controller/Net/IWebSocketManager.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Jellyfin.Data.Events; +using Microsoft.AspNetCore.Http; + +namespace MediaBrowser.Controller.Net +{ + /// + /// Interface IHttpServer. + /// + public interface IWebSocketManager + { + /// + /// Occurs when [web socket connected]. + /// + event EventHandler> WebSocketConnected; + + /// + /// Inits this instance. + /// + /// The websocket listeners. + void Init(IEnumerable listeners); + + /// + /// The HTTP request handler. + /// + /// The current HTTP context. + /// The task. + Task WebSocketRequestHandler(HttpContext context); + } +} -- cgit v1.2.3 From 2f79c3095bb742136ff83141f42e344b33c3a45f Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Thu, 3 Sep 2020 11:54:38 +0200 Subject: Fix startup message --- Emby.Server.Implementations/ApplicationHost.cs | 4 +++- .../Middleware/ServerStartupMessageMiddleware.cs | 13 ++++++++++++- Jellyfin.Server/Startup.cs | 3 +-- MediaBrowser.Controller/IServerApplicationHost.cs | 2 ++ 4 files changed, 18 insertions(+), 4 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index c8af6b73a..8e9a581ea 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -132,6 +132,8 @@ namespace Emby.Server.Implementations /// public bool CanSelfRestart => _startupOptions.RestartPath != null; + public bool CoreStartupHasCompleted { get; private set; } + public virtual bool CanLaunchWebBrowser { get @@ -446,7 +448,7 @@ namespace Emby.Server.Implementations Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed); Logger.LogInformation("Core startup complete"); - + CoreStartupHasCompleted = true; stopWatch.Restart(); await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false); Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed); diff --git a/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs b/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs index 4f347d6d3..ea81c03a2 100644 --- a/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs +++ b/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs @@ -1,5 +1,6 @@ using System.Net.Mime; using System.Threading.Tasks; +using MediaBrowser.Controller; using MediaBrowser.Model.Globalization; using Microsoft.AspNetCore.Http; @@ -25,10 +26,20 @@ namespace Jellyfin.Server.Middleware /// Executes the middleware action. /// /// The current HTTP context. + /// The server application host. /// The localization manager. /// The async task. - public async Task Invoke(HttpContext httpContext, ILocalizationManager localizationManager) + public async Task Invoke( + HttpContext httpContext, + IServerApplicationHost serverApplicationHost, + ILocalizationManager localizationManager) { + if (serverApplicationHost.CoreStartupHasCompleted) + { + await _next(httpContext).ConfigureAwait(false); + return; + } + var message = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; httpContext.Response.ContentType = MediaTypeNames.Text.Html; diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 80f679420..c197888da 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -123,6 +123,7 @@ namespace Jellyfin.Server app.UseCorsOptionsResponse(); app.UseBaseUrlRedirection(); app.UseWebSocketHandler(); + app.UseServerStartupMessage(); app.UseEndpoints(endpoints => { @@ -133,8 +134,6 @@ namespace Jellyfin.Server } }); - app.UseServerStartupMessage(); - // Add type descriptor for legacy datetime parsing. TypeDescriptor.AddAttributes(typeof(DateTime?), new TypeConverterAttribute(typeof(DateTimeTypeConverter))); } diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index d482c19d9..9f4c00e1c 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -20,6 +20,8 @@ namespace MediaBrowser.Controller IServiceProvider ServiceProvider { get; } + bool CoreStartupHasCompleted { get; } + bool CanLaunchWebBrowser { get; } /// -- cgit v1.2.3 From 85844a84b68f7da07695c4e3fa4d187acc559797 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 3 Sep 2020 06:48:19 -0600 Subject: Remove ResponseHeadersRead where applicable --- Emby.Dlna/PlayTo/SsdpHttpClient.cs | 1 - Emby.Server.Implementations/ApplicationHost.cs | 2 +- Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs | 9 ++++----- .../LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs | 6 +++--- MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs | 1 - 5 files changed, 8 insertions(+), 11 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs index 8683c8997..a2f49802e 100644 --- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs +++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs @@ -4,7 +4,6 @@ using System; using System.Globalization; using System.IO; using System.Net.Http; -using System.Net.Http.Headers; using System.Net.Mime; using System.Text; using System.Threading; diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 006060079..fbf4aef8b 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1299,7 +1299,7 @@ namespace Emby.Server.Implementations { using var request = new HttpRequestMessage(HttpMethod.Post, apiUrl); using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + .SendAsync(request, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); var result = await System.Text.Json.JsonSerializer.DeserializeAsync(stream, JsonDefaults.GetOptions(), cancellationToken).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 1543badf0..5c53d4231 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -607,12 +607,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings HttpRequestMessage options, bool enableRetry, ListingsProviderInfo providerInfo, - CancellationToken cancellationToken, - HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) + CancellationToken cancellationToken) { try { - return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false); + return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, cancellationToken).ConfigureAwait(false); } catch (HttpException ex) { @@ -671,7 +670,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings using var options = new HttpRequestMessage(HttpMethod.Put, ApiUrl + "/lineups/" + info.ListingsId); options.Headers.TryAddWithoutValidation("token", token); - using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, cancellationToken).ConfigureAwait(false); } private async Task HasLineup(ListingsProviderInfo info, CancellationToken cancellationToken) @@ -695,7 +694,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings try { - using var httpResponse = await Send(options, false, null, cancellationToken, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); + using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false); await using var stream = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); using var response = httpResponse.Content; var root = await _jsonSerializer.DeserializeFromStreamAsync(stream).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 28e30fac8..fec7c4782b 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); - using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); var lineup = await JsonSerializer.DeserializeAsync>(stream, cancellationToken: cancellationToken) .ConfigureAwait(false) ?? new List(); @@ -127,7 +127,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun try { using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)), cancellationToken) .ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); var discoverResponse = await JsonSerializer.DeserializeAsync(stream, cancellationToken: cancellationToken) @@ -173,7 +173,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), cancellationToken) .ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); using var sr = new StreamReader(stream, System.Text.Encoding.UTF8); diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs index 8414c9328..f06face66 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs @@ -8,7 +8,6 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; -- cgit v1.2.3 From d8a0edc511f9a0a01211c11d4ad32bf0fd154de1 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 3 Sep 2020 07:20:33 -0600 Subject: Revert "Remove ResponseHeadersRead where applicable" This reverts commit 85844a84b68f7da07695c4e3fa4d187acc559797. --- Emby.Dlna/PlayTo/SsdpHttpClient.cs | 1 + Emby.Server.Implementations/ApplicationHost.cs | 2 +- Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs | 9 +++++---- .../LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs | 6 +++--- MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs | 1 + 5 files changed, 11 insertions(+), 8 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs index a2f49802e..8683c8997 100644 --- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs +++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs @@ -4,6 +4,7 @@ using System; using System.Globalization; using System.IO; using System.Net.Http; +using System.Net.Http.Headers; using System.Net.Mime; using System.Text; using System.Threading; diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index fbf4aef8b..006060079 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1299,7 +1299,7 @@ namespace Emby.Server.Implementations { using var request = new HttpRequestMessage(HttpMethod.Post, apiUrl); using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .SendAsync(request, cancellationToken).ConfigureAwait(false); + .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); var result = await System.Text.Json.JsonSerializer.DeserializeAsync(stream, JsonDefaults.GetOptions(), cancellationToken).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 5c53d4231..1543badf0 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -607,11 +607,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings HttpRequestMessage options, bool enableRetry, ListingsProviderInfo providerInfo, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { try { - return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, cancellationToken).ConfigureAwait(false); + return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false); } catch (HttpException ex) { @@ -670,7 +671,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings using var options = new HttpRequestMessage(HttpMethod.Put, ApiUrl + "/lineups/" + info.ListingsId); options.Headers.TryAddWithoutValidation("token", token); - using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, cancellationToken).ConfigureAwait(false); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); } private async Task HasLineup(ListingsProviderInfo info, CancellationToken cancellationToken) @@ -694,7 +695,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings try { - using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false); + using var httpResponse = await Send(options, false, null, cancellationToken, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); await using var stream = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); using var response = httpResponse.Content; var root = await _jsonSerializer.DeserializeFromStreamAsync(stream).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index fec7c4782b..28e30fac8 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); - using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, cancellationToken).ConfigureAwait(false); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); var lineup = await JsonSerializer.DeserializeAsync>(stream, cancellationToken: cancellationToken) .ConfigureAwait(false) ?? new List(); @@ -127,7 +127,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun try { using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)), cancellationToken) + .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); var discoverResponse = await JsonSerializer.DeserializeAsync(stream, cancellationToken: cancellationToken) @@ -173,7 +173,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), cancellationToken) + .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); using var sr = new StreamReader(stream, System.Text.Encoding.UTF8); diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs index f06face66..8414c9328 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; -- cgit v1.2.3 From 9a74ace84bed207952c0b34a820dcb231bb8b805 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 5 Sep 2020 09:20:58 -0600 Subject: Add flag for startup completed --- Emby.Server.Implementations/ApplicationHost.cs | 3 ++- MediaBrowser.Model/System/PublicSystemInfo.cs | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index c37e87d96..7b70be88f 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1137,7 +1137,8 @@ namespace Emby.Server.Implementations Id = SystemId, OperatingSystem = OperatingSystem.Id.ToString(), ServerName = FriendlyName, - LocalAddress = localAddress + LocalAddress = localAddress, + StartupCompleted = CoreStartupHasCompleted }; } diff --git a/MediaBrowser.Model/System/PublicSystemInfo.cs b/MediaBrowser.Model/System/PublicSystemInfo.cs index b6196a43f..012001aea 100644 --- a/MediaBrowser.Model/System/PublicSystemInfo.cs +++ b/MediaBrowser.Model/System/PublicSystemInfo.cs @@ -39,5 +39,11 @@ namespace MediaBrowser.Model.System /// /// The id. public string Id { get; set; } + + /// + /// Gets or sets a value indicating whether startup is completed. + /// + /// The startup completion status. + public bool StartupCompleted { get; set; } } } -- cgit v1.2.3 From db9bcdcdc9318c9158249a669922bd464fe8b08e Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 5 Sep 2020 18:48:19 -0600 Subject: Use proper StartupCompleted flag --- Emby.Server.Implementations/ApplicationHost.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 7b70be88f..eca40b0f2 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -120,6 +120,7 @@ namespace Emby.Server.Implementations private readonly INetworkManager _networkManager; private readonly IXmlSerializer _xmlSerializer; private readonly IStartupOptions _startupOptions; + private readonly IConfigurationManager _configurationManager; private IMediaEncoder _mediaEncoder; private ISessionManager _sessionManager; @@ -238,18 +239,27 @@ namespace Emby.Server.Implementations public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. public ApplicationHost( IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IStartupOptions options, IFileSystem fileSystem, INetworkManager networkManager, - IServiceCollection serviceCollection) + IServiceCollection serviceCollection, + IConfigurationManager configurationManager) { _xmlSerializer = new MyXmlSerializer(); ServiceCollection = serviceCollection; + _configurationManager = configurationManager; _networkManager = networkManager; networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets; @@ -1138,7 +1148,7 @@ namespace Emby.Server.Implementations OperatingSystem = OperatingSystem.Id.ToString(), ServerName = FriendlyName, LocalAddress = localAddress, - StartupCompleted = CoreStartupHasCompleted + StartupCompleted = _configurationManager.CommonConfiguration.IsStartupWizardCompleted }; } -- cgit v1.2.3 From 26c432b56489ec68c0cb708982c6939bf8e6e0e5 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 6 Sep 2020 14:27:31 -0600 Subject: Rename StartupCompleted to StartupWizardCompleted --- Emby.Server.Implementations/ApplicationHost.cs | 2 +- MediaBrowser.Model/System/PublicSystemInfo.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index eca40b0f2..a9baf893c 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1148,7 +1148,7 @@ namespace Emby.Server.Implementations OperatingSystem = OperatingSystem.Id.ToString(), ServerName = FriendlyName, LocalAddress = localAddress, - StartupCompleted = _configurationManager.CommonConfiguration.IsStartupWizardCompleted + StartupWizardCompleted = _configurationManager.CommonConfiguration.IsStartupWizardCompleted }; } diff --git a/MediaBrowser.Model/System/PublicSystemInfo.cs b/MediaBrowser.Model/System/PublicSystemInfo.cs index 012001aea..d2f7556a5 100644 --- a/MediaBrowser.Model/System/PublicSystemInfo.cs +++ b/MediaBrowser.Model/System/PublicSystemInfo.cs @@ -24,7 +24,7 @@ namespace MediaBrowser.Model.System public string Version { get; set; } /// - /// The product name. This is the AssemblyProduct name. + /// Gets or sets the product name. This is the AssemblyProduct name. /// public string ProductName { get; set; } @@ -41,9 +41,9 @@ namespace MediaBrowser.Model.System public string Id { get; set; } /// - /// Gets or sets a value indicating whether startup is completed. + /// Gets or sets a value indicating whether the startup wizard is completed. /// /// The startup completion status. - public bool StartupCompleted { get; set; } + public bool StartupWizardCompleted { get; set; } } } -- cgit v1.2.3 From cf87b3afb7c64bd1102dc9bd445f75a4042e0442 Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Mon, 7 Sep 2020 12:28:48 +0100 Subject: Remove excess code. --- Emby.Server.Implementations/ApplicationHost.cs | 4 ---- Emby.Server.Implementations/Udp/UdpServer.cs | 6 ------ MediaBrowser.Controller/IServerApplicationHost.cs | 2 -- 3 files changed, 12 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index c37e87d96..318a853c5 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1431,10 +1431,6 @@ namespace Emby.Server.Implementations } } - public virtual void EnableLoopback(string appName) - { - } - private bool _disposed = false; /// diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index 73fcbcec3..b7a59cee2 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -68,12 +68,6 @@ namespace Emby.Server.Implementations.Udp { _logger.LogError(ex, "Error sending response message"); } - - var parts = messageText.Split('|'); - if (parts.Length > 1) - { - _appHost.EnableLoopback(parts[1]); - } } else { diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 9f4c00e1c..cfad17fb7 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -114,8 +114,6 @@ namespace MediaBrowser.Controller /// is false. void LaunchUrl(string url); - void EnableLoopback(string appName); - IEnumerable GetWakeOnLanInfo(); string ExpandVirtualPath(string path); -- cgit v1.2.3 From a2e90170da52c8cde92a2e813dbaf930c6697dc2 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 7 Sep 2020 14:21:30 -0600 Subject: Use existing configuration manager --- Emby.Server.Implementations/ApplicationHost.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) (limited to 'Emby.Server.Implementations/ApplicationHost.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 93d55cc67..642e2fdbe 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -120,7 +120,6 @@ namespace Emby.Server.Implementations private readonly INetworkManager _networkManager; private readonly IXmlSerializer _xmlSerializer; private readonly IStartupOptions _startupOptions; - private readonly IConfigurationManager _configurationManager; private IMediaEncoder _mediaEncoder; private ISessionManager _sessionManager; @@ -247,19 +246,16 @@ namespace Emby.Server.Implementations /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. public ApplicationHost( IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IStartupOptions options, IFileSystem fileSystem, INetworkManager networkManager, - IServiceCollection serviceCollection, - IConfigurationManager configurationManager) + IServiceCollection serviceCollection) { _xmlSerializer = new MyXmlSerializer(); ServiceCollection = serviceCollection; - _configurationManager = configurationManager; _networkManager = networkManager; networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets; @@ -1148,7 +1144,7 @@ namespace Emby.Server.Implementations OperatingSystem = OperatingSystem.Id.ToString(), ServerName = FriendlyName, LocalAddress = localAddress, - StartupWizardCompleted = _configurationManager.CommonConfiguration.IsStartupWizardCompleted + StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted }; } -- cgit v1.2.3