From 16ca8c753618395af2e186744570768e7a76bafc Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Thu, 1 Apr 2021 10:42:00 -0400 Subject: [PATCH 01/82] Remove unused SessionManager methods --- .../Session/SessionManager.cs | 16 +--------------- .../Session/ISessionManager.cs | 15 --------------- 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 10e28c33a5..e06e48ca67 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1437,11 +1437,6 @@ namespace Emby.Server.Implementations.Session return AuthenticateNewSessionInternal(request, true); } - public Task CreateNewSession(AuthenticationRequest request) - { - return AuthenticateNewSessionInternal(request, false); - } - public Task AuthenticateQuickConnect(AuthenticationRequest request, string token) { var result = _authRepo.Get(new AuthenticationInfoQuery() @@ -1784,18 +1779,9 @@ namespace Emby.Server.Implementations.Session } var item = _libraryManager.GetItemById(new Guid(itemId)); - - var info = GetItemInfo(item, null); - - ReportNowViewingItem(sessionId, info); - } - - /// - public void ReportNowViewingItem(string sessionId, BaseItemDto item) - { var session = GetSession(sessionId); - session.NowViewingItem = item; + session.NowViewingItem = GetItemInfo(item, null); } /// diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 6c06dcad58..9eb486534c 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -8,7 +8,6 @@ using Jellyfin.Data.Events; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Security; -using MediaBrowser.Model.Dto; using MediaBrowser.Model.Session; using MediaBrowser.Model.SyncPlay; @@ -263,13 +262,6 @@ namespace MediaBrowser.Controller.Session /// The item identifier. void ReportNowViewingItem(string sessionId, string itemId); - /// - /// Reports the now viewing item. - /// - /// The session identifier. - /// The item. - void ReportNowViewingItem(string sessionId, BaseItemDto item); - /// /// Authenticates the new session. /// @@ -285,13 +277,6 @@ namespace MediaBrowser.Controller.Session /// Task{SessionInfo}. Task AuthenticateQuickConnect(AuthenticationRequest request, string token); - /// - /// Creates the new session. - /// - /// The request. - /// Task<AuthenticationResult>. - Task CreateNewSession(AuthenticationRequest request); - /// /// Reports the capabilities. /// From 1c501b17d7b6ceeba3450e0be768cfdbf7d581d0 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Thu, 1 Apr 2021 11:08:39 -0400 Subject: [PATCH 02/82] Add ApiKey entity and associated relationships --- Jellyfin.Data/Entities/Security/ApiKey.cs | 50 +++++++++++++++++++ Jellyfin.Server.Implementations/JellyfinDb.cs | 7 +++ 2 files changed, 57 insertions(+) create mode 100644 Jellyfin.Data/Entities/Security/ApiKey.cs diff --git a/Jellyfin.Data/Entities/Security/ApiKey.cs b/Jellyfin.Data/Entities/Security/ApiKey.cs new file mode 100644 index 0000000000..2a3ad09c43 --- /dev/null +++ b/Jellyfin.Data/Entities/Security/ApiKey.cs @@ -0,0 +1,50 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities.Security +{ + /// + /// An entity representing an API key. + /// + public class ApiKey + { + /// + /// Initializes a new instance of the class. + /// + /// The name. + public ApiKey(string name) + { + Name = name; + + AccessToken = Guid.NewGuid(); + DateCreated = DateTime.UtcNow; + } + + /// + /// Gets the id. + /// + /// + /// Identity, Indexed, Required. + /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; private set; } + + /// + /// Gets the date created. + /// + public DateTime DateCreated { get; private set; } + + /// + /// Gets or sets the name. + /// + [MaxLength(64)] + [StringLength(64)] + public string Name { get; set; } + + /// + /// Gets or sets the access token. + /// + public Guid AccessToken { get; set; } + } +} diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index db648472d1..0559e57833 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using Jellyfin.Data.Entities; +using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Interfaces; using Microsoft.EntityFrameworkCore; @@ -29,6 +30,8 @@ namespace Jellyfin.Server.Implementations public virtual DbSet ActivityLogs { get; set; } + public virtual DbSet ApiKeys { get; set; } + public virtual DbSet DisplayPreferences { get; set; } public virtual DbSet ImageInfos { get; set; } @@ -196,6 +199,10 @@ namespace Jellyfin.Server.Implementations // Indexes + modelBuilder.Entity() + .HasIndex(entity => entity.AccessToken) + .IsUnique(); + modelBuilder.Entity() .HasIndex(entity => entity.Username) .IsUnique(); From 499785bebb5699c61b211dcb6ea0ee2001effa6f Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Thu, 1 Apr 2021 17:08:22 -0400 Subject: [PATCH 03/82] Use new entities for API key endpoints --- Jellyfin.Api/Controllers/ApiKeyController.cs | 52 +++++-------- .../Security/AuthenticationManager.cs | 74 +++++++++++++++++++ .../Security/IAuthenticationManager.cs | 34 +++++++++ 3 files changed, 126 insertions(+), 34 deletions(-) create mode 100644 Jellyfin.Server.Implementations/Security/AuthenticationManager.cs create mode 100644 MediaBrowser.Controller/Security/IAuthenticationManager.cs diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs index 8c43d786a7..96efde5fbd 100644 --- a/Jellyfin.Api/Controllers/ApiKeyController.cs +++ b/Jellyfin.Api/Controllers/ApiKeyController.cs @@ -1,10 +1,8 @@ using System; using System.ComponentModel.DataAnnotations; -using System.Globalization; +using System.Threading.Tasks; using Jellyfin.Api.Constants; -using MediaBrowser.Controller; using MediaBrowser.Controller.Security; -using MediaBrowser.Controller.Session; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -18,24 +16,15 @@ namespace Jellyfin.Api.Controllers [Route("Auth")] public class ApiKeyController : BaseJellyfinApiController { - private readonly ISessionManager _sessionManager; - private readonly IServerApplicationHost _appHost; - private readonly IAuthenticationRepository _authRepo; + private readonly IAuthenticationManager _authenticationManager; /// /// Initializes a new instance of the class. /// - /// Instance of interface. - /// Instance of interface. - /// Instance of interface. - public ApiKeyController( - ISessionManager sessionManager, - IServerApplicationHost appHost, - IAuthenticationRepository authRepo) + /// Instance of interface. + public ApiKeyController(IAuthenticationManager authenticationManager) { - _sessionManager = sessionManager; - _appHost = appHost; - _authRepo = authRepo; + _authenticationManager = authenticationManager; } /// @@ -46,14 +35,15 @@ namespace Jellyfin.Api.Controllers [HttpGet("Keys")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetKeys() + public async Task>> GetKeys() { - var result = _authRepo.Get(new AuthenticationInfoQuery - { - HasUser = false - }); + var keys = await _authenticationManager.GetApiKeys(); - return result; + return new QueryResult + { + Items = keys, + TotalRecordCount = keys.Count + }; } /// @@ -65,17 +55,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("Keys")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult CreateKey([FromQuery, Required] string app) + public async Task CreateKey([FromQuery, Required] string app) { - _authRepo.Create(new AuthenticationInfo - { - AppName = app, - AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), - DateCreated = DateTime.UtcNow, - DeviceId = _appHost.SystemId, - DeviceName = _appHost.FriendlyName, - AppVersion = _appHost.ApplicationVersionString - }); + await _authenticationManager.CreateApiKey(app).ConfigureAwait(false); + return NoContent(); } @@ -88,9 +71,10 @@ namespace Jellyfin.Api.Controllers [HttpDelete("Keys/{key}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult RevokeKey([FromRoute, Required] string key) + public async Task RevokeKey([FromRoute, Required] Guid key) { - _sessionManager.RevokeToken(key); + await _authenticationManager.DeleteApiKey(key).ConfigureAwait(false); + return NoContent(); } } diff --git a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs new file mode 100644 index 0000000000..37b8ee6e03 --- /dev/null +++ b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Jellyfin.Data.Entities.Security; +using MediaBrowser.Controller.Security; +using Microsoft.EntityFrameworkCore; + +namespace Jellyfin.Server.Implementations.Security +{ + /// + public class AuthenticationManager : IAuthenticationManager + { + private readonly JellyfinDbProvider _dbProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The database provider. + public AuthenticationManager(JellyfinDbProvider dbProvider) + { + _dbProvider = dbProvider; + } + + /// + public async Task CreateApiKey(string name) + { + await using var dbContext = _dbProvider.CreateContext(); + + dbContext.ApiKeys.Add(new ApiKey(name)); + + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + + /// + public async Task> GetApiKeys() + { + await using var dbContext = _dbProvider.CreateContext(); + + return await dbContext.ApiKeys + .AsAsyncEnumerable() + .Select(key => new AuthenticationInfo + { + AppName = key.Name, + AccessToken = key.AccessToken.ToString("N", CultureInfo.InvariantCulture), + DateCreated = key.DateCreated, + DeviceId = string.Empty, + DeviceName = string.Empty, + AppVersion = string.Empty + }).ToListAsync().ConfigureAwait(false); + } + + /// + public async Task DeleteApiKey(Guid id) + { + await using var dbContext = _dbProvider.CreateContext(); + + var key = await dbContext.ApiKeys + .AsQueryable() + .Where(apiKey => apiKey.AccessToken == id) + .FirstOrDefaultAsync(); + + if (key == null) + { + return; + } + + dbContext.Remove(key); + + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + } +} diff --git a/MediaBrowser.Controller/Security/IAuthenticationManager.cs b/MediaBrowser.Controller/Security/IAuthenticationManager.cs new file mode 100644 index 0000000000..46d0c66224 --- /dev/null +++ b/MediaBrowser.Controller/Security/IAuthenticationManager.cs @@ -0,0 +1,34 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Security +{ + /// + /// Handles the retrieval and storage of API keys. + /// + public interface IAuthenticationManager + { + /// + /// Creates an API key. + /// + /// The name of the key. + /// A task representing the creation of the key. + Task CreateApiKey(string name); + + /// + /// Gets the API keys. + /// + /// A task representing the retrieval of the API keys. + Task> GetApiKeys(); + + /// + /// Deletes an API key with the provided access token. + /// + /// The access token. + /// A task representing the deletion of the API key. + Task DeleteApiKey(Guid accessToken); + } +} From 271b4cadb75a09a98dc4aa16ad7fd71af2797980 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 9 Apr 2021 23:16:07 -0400 Subject: [PATCH 04/82] Add device entity. --- Jellyfin.Data/Entities/Security/Device.cs | 81 +++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 Jellyfin.Data/Entities/Security/Device.cs diff --git a/Jellyfin.Data/Entities/Security/Device.cs b/Jellyfin.Data/Entities/Security/Device.cs new file mode 100644 index 0000000000..8e8b40aa11 --- /dev/null +++ b/Jellyfin.Data/Entities/Security/Device.cs @@ -0,0 +1,81 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Jellyfin.Data.Entities.Security +{ + /// + /// An entity representing a device. + /// + public class Device + { + /// + /// Initializes a new instance of the class. + /// + /// The user id. + /// The app name. + /// The app version. + /// The device name. + /// The device id. + public Device(Guid userId, string appName, string appVersion, string deviceName, string deviceId) + { + UserId = userId; + AppName = appName; + AppVersion = appVersion; + DeviceName = deviceName; + DeviceId = deviceId; + + DateCreated = DateTime.UtcNow; + DateLastActivity = DateCreated; + } + + public int Id { get; private set; } + + /// + /// Gets the user id. + /// + public Guid UserId { get; private set; } + + /// + /// Gets or sets the app name. + /// + [MaxLength(64)] + [StringLength(64)] + public string AppName { get; set; } + + /// + /// Gets or sets the app version. + /// + [MaxLength(32)] + [StringLength(32)] + public string AppVersion { get; set; } + + /// + /// Gets or sets the device name. + /// + [MaxLength(64)] + [StringLength(64)] + public string DeviceName { get; set; } + + /// + /// Gets or sets the device id. + /// + [MaxLength(256)] + [StringLength(256)] + public string DeviceId { get; set; } + + /// + /// Gets or sets a value indicating whether this device is active. + /// + public bool IsActive { get; set; } + + /// + /// Gets the date this device was created. + /// + public DateTime DateCreated { get; private set; } + + /// + /// Gets or sets the date of last activity. + /// + public DateTime DateLastActivity { get; set; } + } +} From 9cd53523583b2b3e12ba927ce10e9685ea78d255 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sat, 10 Apr 2021 16:03:20 -0400 Subject: [PATCH 05/82] Add device options entity --- .../Entities/Security/DeviceOptions.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 Jellyfin.Data/Entities/Security/DeviceOptions.cs diff --git a/Jellyfin.Data/Entities/Security/DeviceOptions.cs b/Jellyfin.Data/Entities/Security/DeviceOptions.cs new file mode 100644 index 0000000000..531f66c627 --- /dev/null +++ b/Jellyfin.Data/Entities/Security/DeviceOptions.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities.Security +{ + /// + /// An entity representing custom options for a device. + /// + public class DeviceOptions + { + /// + /// Initializes a new instance of the class. + /// + /// The device id. + public DeviceOptions(string deviceId) + { + DeviceId = deviceId; + } + + /// + /// Gets the id. + /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; private set; } + + /// + /// Gets the device id. + /// + public string DeviceId { get; private set; } + + /// + /// Gets or sets the custom name. + /// + public string? CustomName { get; set; } + } +} From e6f1ffdc8d2d430cbe4ac1d797e455950bb79ed3 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sat, 10 Apr 2021 16:15:59 -0400 Subject: [PATCH 06/82] Add device entities to schema. --- Jellyfin.Server.Implementations/JellyfinDb.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index 0559e57833..32a857047c 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -32,6 +32,10 @@ namespace Jellyfin.Server.Implementations public virtual DbSet ApiKeys { get; set; } + public virtual DbSet Devices { get; set; } + + public virtual DbSet DeviceOptions { get; set; } + public virtual DbSet DisplayPreferences { get; set; } public virtual DbSet ImageInfos { get; set; } From 98e19c9fd34c831cce445a5d4aa0a7f277f16237 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sat, 10 Apr 2021 16:16:08 -0400 Subject: [PATCH 07/82] Add device indexes --- Jellyfin.Server.Implementations/JellyfinDb.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index 32a857047c..059e884e5f 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -211,6 +211,13 @@ namespace Jellyfin.Server.Implementations .HasIndex(entity => entity.Username) .IsUnique(); + modelBuilder.Entity() + .HasIndex(entity => new { entity.DeviceId, entity.DateLastActivity }); + + modelBuilder.Entity() + .HasIndex(entity => entity.DeviceId) + .IsUnique(); + modelBuilder.Entity() .HasIndex(entity => new { entity.UserId, entity.ItemId, entity.Client }) .IsUnique(); From f47fe308b1f5a73f684a8b69754d15d6c2c36b13 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sat, 10 Apr 2021 16:17:02 -0400 Subject: [PATCH 08/82] Add navigation property to device entity --- Jellyfin.Data/Entities/Security/Device.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Jellyfin.Data/Entities/Security/Device.cs b/Jellyfin.Data/Entities/Security/Device.cs index 8e8b40aa11..17d17f5946 100644 --- a/Jellyfin.Data/Entities/Security/Device.cs +++ b/Jellyfin.Data/Entities/Security/Device.cs @@ -26,6 +26,9 @@ namespace Jellyfin.Data.Entities.Security DateCreated = DateTime.UtcNow; DateLastActivity = DateCreated; + + // Non-nullable for EF Core, as this is a required relationship. + User = null!; } public int Id { get; private set; } @@ -77,5 +80,10 @@ namespace Jellyfin.Data.Entities.Security /// Gets or sets the date of last activity. /// public DateTime DateLastActivity { get; set; } + + /// + /// Gets the user. + /// + public User User { get; private set; } } } From 44e71774b17942034691d6a2c630cd687b23bceb Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sat, 10 Apr 2021 16:17:36 -0400 Subject: [PATCH 09/82] Rewrite device manager using EF Core --- .../ApplicationHost.cs | 2 - .../Security/AuthenticationRepository.cs | 4 +- .../Session/SessionManager.cs | 2 +- Jellyfin.Api/Controllers/DevicesController.cs | 14 +- .../Devices/DeviceManager.cs | 124 ++++++++++-------- Jellyfin.Server/CoreAppHost.cs | 3 + .../Devices/IDeviceManager.cs | 9 +- .../Security/IAuthenticationRepository.cs | 2 +- MediaBrowser.Model/Devices/DeviceOptions.cs | 9 -- 9 files changed, 89 insertions(+), 80 deletions(-) rename {Emby.Server.Implementations => Jellyfin.Server.Implementations}/Devices/DeviceManager.cs (50%) delete mode 100644 MediaBrowser.Model/Devices/DeviceOptions.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 3846de5fd4..f2ed20fbcb 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -644,8 +644,6 @@ namespace Emby.Server.Implementations ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); ServiceCollection.AddSingleton(); diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs index 4bc12f44a4..0d0a2b1df7 100644 --- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs +++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs @@ -6,9 +6,9 @@ using System.Globalization; using System.IO; using System.Linq; using Emby.Server.Implementations.Data; +using Jellyfin.Data.Entities.Security; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Security; -using MediaBrowser.Model.Devices; using MediaBrowser.Model.Querying; using Microsoft.Extensions.Logging; using SQLitePCL.pretty; @@ -357,7 +357,7 @@ namespace Emby.Server.Implementations.Security { statement.TryBind("@DeviceId", deviceId); - var result = new DeviceOptions(); + var result = new DeviceOptions(deviceId); foreach (var row in statement.ExecuteQuery()) { diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index e06e48ca67..a47a1f56f8 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using MediaBrowser.Common.Events; @@ -24,7 +25,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Devices; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Library; diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index b3e3490c2a..3ca6488d91 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -1,6 +1,8 @@ using System; using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Data.Entities.Security; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; @@ -47,10 +49,10 @@ namespace Jellyfin.Api.Controllers /// An containing the list of devices. [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) + public async Task>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) { var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty }; - return _deviceManager.GetDevices(deviceQuery); + return await _deviceManager.GetDevices(deviceQuery); } /// @@ -63,9 +65,9 @@ namespace Jellyfin.Api.Controllers [HttpGet("Info")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetDeviceInfo([FromQuery, Required] string id) + public async Task> GetDeviceInfo([FromQuery, Required] string id) { - var deviceInfo = _deviceManager.GetDevice(id); + var deviceInfo = await _deviceManager.GetDevice(id).ConfigureAwait(false); if (deviceInfo == null) { return NotFound(); @@ -106,7 +108,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Options")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UpdateDeviceOptions( + public async Task UpdateDeviceOptions( [FromQuery, Required] string id, [FromBody, Required] DeviceOptions deviceOptions) { @@ -116,7 +118,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - _deviceManager.UpdateDeviceOptions(id, deviceOptions); + await _deviceManager.UpdateDeviceOptions(id, deviceOptions).ConfigureAwait(false); return NoContent(); } diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs similarity index 50% rename from Emby.Server.Implementations/Devices/DeviceManager.cs rename to Jellyfin.Server.Implementations/Devices/DeviceManager.cs index da5047d244..c942678d97 100644 --- a/Emby.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -1,78 +1,100 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Security; using MediaBrowser.Model.Devices; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; +using Microsoft.EntityFrameworkCore; -namespace Emby.Server.Implementations.Devices +namespace Jellyfin.Server.Implementations.Devices { public class DeviceManager : IDeviceManager { + private readonly JellyfinDbProvider _dbProvider; private readonly IUserManager _userManager; - private readonly IAuthenticationRepository _authRepo; private readonly ConcurrentDictionary _capabilitiesMap = new (); - public DeviceManager(IAuthenticationRepository authRepo, IUserManager userManager) + /// + /// Initializes a new instance of the class. + /// + /// The database provider. + /// The user manager. + public DeviceManager(JellyfinDbProvider dbProvider, IUserManager userManager) { + _dbProvider = dbProvider; _userManager = userManager; - _authRepo = authRepo; } - public event EventHandler>> DeviceOptionsUpdated; + /// + public event EventHandler>>? DeviceOptionsUpdated; + /// public void SaveCapabilities(string deviceId, ClientCapabilities capabilities) { _capabilitiesMap[deviceId] = capabilities; } - public void UpdateDeviceOptions(string deviceId, DeviceOptions options) + /// + public async Task UpdateDeviceOptions(string deviceId, DeviceOptions options) { - _authRepo.UpdateDeviceOptions(deviceId, options); + await using var dbContext = _dbProvider.CreateContext(); + await dbContext.Database + .ExecuteSqlRawAsync($"UPDATE [DeviceOptions] SET [CustomName] = ${options.CustomName}") + .ConfigureAwait(false); DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs>(new Tuple(deviceId, options))); } - public DeviceOptions GetDeviceOptions(string deviceId) + /// + public DeviceOptions? GetDeviceOptions(string deviceId) { - return _authRepo.GetDeviceOptions(deviceId); + using var dbContext = _dbProvider.CreateContext(); + return dbContext.DeviceOptions + .AsQueryable() + .FirstOrDefault(d => d.DeviceId == deviceId); } + /// public ClientCapabilities GetCapabilities(string id) { - return _capabilitiesMap.TryGetValue(id, out ClientCapabilities result) + return _capabilitiesMap.TryGetValue(id, out ClientCapabilities? result) ? result : new ClientCapabilities(); } - public DeviceInfo GetDevice(string id) + /// + public async Task GetDevice(string id) { - var session = _authRepo.Get(new AuthenticationInfoQuery - { - DeviceId = id - }).Items.FirstOrDefault(); + await using var dbContext = _dbProvider.CreateContext(); + var device = await dbContext.Devices + .AsQueryable() + .Where(d => d.DeviceId == id) + .OrderByDescending(d => d.DateLastActivity) + .Include(d => d.User) + .FirstOrDefaultAsync() + .ConfigureAwait(false); - var device = session == null ? null : ToDeviceInfo(session); + var deviceInfo = device == null ? null : ToDeviceInfo(device); - return device; + return deviceInfo; } - public QueryResult GetDevices(DeviceQuery query) + /// + public async Task> GetDevices(DeviceQuery query) { - IEnumerable sessions = _authRepo.Get(new AuthenticationInfoQuery - { - // UserId = query.UserId - HasUser = true - }).Items; + await using var dbContext = _dbProvider.CreateContext(); + var sessions = dbContext.Devices + .AsQueryable() + .OrderBy(d => d.DeviceId) + .ThenByDescending(d => d.DateLastActivity) + .AsAsyncEnumerable(); // TODO: DeviceQuery doesn't seem to be used from client. Not even Swagger. if (query.SupportsSync.HasValue) @@ -89,28 +111,12 @@ namespace Emby.Server.Implementations.Devices sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId)); } - var array = sessions.Select(ToDeviceInfo).ToArray(); + var array = await sessions.Select(ToDeviceInfo).ToArrayAsync(); return new QueryResult(array); } - private DeviceInfo ToDeviceInfo(AuthenticationInfo authInfo) - { - var caps = GetCapabilities(authInfo.DeviceId); - - return new DeviceInfo - { - AppName = authInfo.AppName, - AppVersion = authInfo.AppVersion, - Id = authInfo.DeviceId, - LastUserId = authInfo.UserId, - LastUserName = authInfo.UserName, - Name = authInfo.DeviceName, - DateLastActivity = authInfo.DateLastActivity, - IconUrl = caps?.IconUrl - }; - } - + /// public bool CanAccessDevice(User user, string deviceId) { if (user == null) @@ -128,17 +134,25 @@ namespace Emby.Server.Implementations.Devices return true; } - if (!user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparer.OrdinalIgnoreCase)) + return user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparer.OrdinalIgnoreCase) + || !GetCapabilities(deviceId).SupportsPersistentIdentifier; + } + + private DeviceInfo ToDeviceInfo(Device authInfo) + { + var caps = GetCapabilities(authInfo.DeviceId); + + return new DeviceInfo { - var capabilities = GetCapabilities(deviceId); - - if (capabilities != null && capabilities.SupportsPersistentIdentifier) - { - return false; - } - } - - return true; + AppName = authInfo.AppName, + AppVersion = authInfo.AppVersion, + Id = authInfo.DeviceId, + LastUserId = authInfo.UserId, + LastUserName = authInfo.User.Username, + Name = authInfo.DeviceName, + DateLastActivity = authInfo.DateLastActivity, + IconUrl = caps.IconUrl + }; } } } diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 94c3ca4a95..b20acae32e 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -9,10 +9,12 @@ using Jellyfin.Api.WebSocketListeners; using Jellyfin.Drawing.Skia; using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Activity; +using Jellyfin.Server.Implementations.Devices; using Jellyfin.Server.Implementations.Events; using Jellyfin.Server.Implementations.Users; using MediaBrowser.Controller; using MediaBrowser.Controller.BaseItemManager; +using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Library; @@ -84,6 +86,7 @@ namespace Jellyfin.Server ServiceCollection.AddSingleton(); ServiceCollection.AddSingleton(); ServiceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); // TODO search the assemblies instead of adding them manually? ServiceCollection.AddSingleton(); diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index 8f0872dba9..aa05ead8fd 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -1,7 +1,9 @@ #pragma warning disable CS1591 using System; +using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Events; using MediaBrowser.Model.Devices; using MediaBrowser.Model.Querying; @@ -18,7 +20,6 @@ namespace MediaBrowser.Controller.Devices /// /// The reported identifier. /// The capabilities. - /// Task. void SaveCapabilities(string reportedId, ClientCapabilities capabilities); /// @@ -33,21 +34,21 @@ namespace MediaBrowser.Controller.Devices /// /// The identifier. /// DeviceInfo. - DeviceInfo GetDevice(string id); + Task GetDevice(string id); /// /// Gets the devices. /// /// The query. /// IEnumerable<DeviceInfo>. - QueryResult GetDevices(DeviceQuery query); + Task> GetDevices(DeviceQuery query); /// /// Determines whether this instance [can access device] the specified user identifier. /// bool CanAccessDevice(User user, string deviceId); - void UpdateDeviceOptions(string deviceId, DeviceOptions options); + Task UpdateDeviceOptions(string deviceId, DeviceOptions options); DeviceOptions GetDeviceOptions(string deviceId); } diff --git a/MediaBrowser.Controller/Security/IAuthenticationRepository.cs b/MediaBrowser.Controller/Security/IAuthenticationRepository.cs index 883b74165c..27f281b71f 100644 --- a/MediaBrowser.Controller/Security/IAuthenticationRepository.cs +++ b/MediaBrowser.Controller/Security/IAuthenticationRepository.cs @@ -1,6 +1,6 @@ #pragma warning disable CS1591 -using MediaBrowser.Model.Devices; +using Jellyfin.Data.Entities.Security; using MediaBrowser.Model.Querying; namespace MediaBrowser.Controller.Security diff --git a/MediaBrowser.Model/Devices/DeviceOptions.cs b/MediaBrowser.Model/Devices/DeviceOptions.cs deleted file mode 100644 index 037ffeb5e8..0000000000 --- a/MediaBrowser.Model/Devices/DeviceOptions.cs +++ /dev/null @@ -1,9 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Devices -{ - public class DeviceOptions - { - public string? CustomName { get; set; } - } -} From 8607b5254142662e79dbf826d43375ce60727cfe Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sat, 10 Apr 2021 16:57:25 -0400 Subject: [PATCH 10/82] Make device/session code async --- Emby.Dlna/PlayTo/PlayToManager.cs | 4 +- .../HttpServer/Security/SessionContext.cs | 11 +-- .../Session/SessionManager.cs | 26 +++--- .../Session/SessionWebSocketListener.cs | 4 +- Jellyfin.Api/Controllers/DevicesController.cs | 4 +- Jellyfin.Api/Controllers/LiveTvController.cs | 26 +++--- .../Controllers/PlaystateController.cs | 20 ++--- Jellyfin.Api/Controllers/SessionController.cs | 72 +++++++++------- .../Controllers/SyncPlayController.cs | 85 ++++++++++--------- Jellyfin.Api/Helpers/RequestHelpers.cs | 14 ++- .../Devices/DeviceManager.cs | 9 +- .../Devices/IDeviceManager.cs | 2 +- .../Net/ISessionContext.cs | 9 +- .../Session/ISessionManager.cs | 6 +- 14 files changed, 160 insertions(+), 132 deletions(-) diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs index 8272e505a0..236ea4d57d 100644 --- a/Emby.Dlna/PlayTo/PlayToManager.cs +++ b/Emby.Dlna/PlayTo/PlayToManager.cs @@ -171,7 +171,9 @@ namespace Emby.Dlna.PlayTo uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture); } - var sessionInfo = _sessionManager.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null); + var sessionInfo = await _sessionManager + .LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null) + .ConfigureAwait(false); var controller = sessionInfo.SessionControllers.OfType().FirstOrDefault(); diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs index 040b6b9e4e..1b295a92d8 100644 --- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; @@ -23,7 +24,7 @@ namespace Emby.Server.Implementations.HttpServer.Security _sessionManager = sessionManager; } - public SessionInfo GetSession(HttpContext requestContext) + public Task GetSession(HttpContext requestContext) { var authorization = _authContext.GetAuthorizationInfo(requestContext); @@ -31,19 +32,19 @@ namespace Emby.Server.Implementations.HttpServer.Security return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp(), user); } - public SessionInfo GetSession(object requestContext) + public Task GetSession(object requestContext) { return GetSession((HttpContext)requestContext); } - public User GetUser(HttpContext requestContext) + public async Task GetUser(HttpContext requestContext) { - var session = GetSession(requestContext); + var session = await GetSession(requestContext).ConfigureAwait(false); return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId); } - public User GetUser(object requestContext) + public Task GetUser(object requestContext) { return GetUser(((HttpRequest)requestContext).HttpContext); } diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index a47a1f56f8..678a27665b 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -254,7 +254,7 @@ namespace Emby.Server.Implementations.Session /// The remote end point. /// The user. /// SessionInfo. - public SessionInfo LogSessionActivity( + public async Task LogSessionActivity( string appName, string appVersion, string deviceId, @@ -280,7 +280,7 @@ namespace Emby.Server.Implementations.Session } var activityDate = DateTime.UtcNow; - var session = GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user); + var session = await GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false); var lastActivityDate = session.LastActivityDate; session.LastActivityDate = activityDate; @@ -458,7 +458,7 @@ namespace Emby.Server.Implementations.Session /// The remote end point. /// The user. /// SessionInfo. - private SessionInfo GetSessionInfo( + private async Task GetSessionInfo( string appName, string appVersion, string deviceId, @@ -477,9 +477,11 @@ namespace Emby.Server.Implementations.Session CheckDisposed(); - var sessionInfo = _activeConnections.GetOrAdd( - key, - k => CreateSession(k, appName, appVersion, deviceId, deviceName, remoteEndPoint, user)); + if (!_activeConnections.TryGetValue(key, out var sessionInfo)) + { + _activeConnections[key] = await CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false); + sessionInfo = _activeConnections[key]; + } sessionInfo.UserId = user?.Id ?? Guid.Empty; sessionInfo.UserName = user?.Username; @@ -502,7 +504,7 @@ namespace Emby.Server.Implementations.Session return sessionInfo; } - private SessionInfo CreateSession( + private async Task CreateSession( string key, string appName, string appVersion, @@ -532,7 +534,7 @@ namespace Emby.Server.Implementations.Session deviceName = "Network Device"; } - var deviceOptions = _deviceManager.GetDeviceOptions(deviceId); + var deviceOptions = await _deviceManager.GetDeviceOptions(deviceId).ConfigureAwait(false); if (string.IsNullOrEmpty(deviceOptions.CustomName)) { sessionInfo.DeviceName = deviceName; @@ -1507,13 +1509,13 @@ namespace Emby.Server.Implementations.Session var token = GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName); - var session = LogSessionActivity( + var session = await LogSessionActivity( request.App, request.AppVersion, request.DeviceId, request.DeviceName, request.RemoteEndPoint, - user); + user).ConfigureAwait(false); var returnResult = new AuthenticationResult { @@ -1811,7 +1813,7 @@ namespace Emby.Server.Implementations.Session } /// - public SessionInfo GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion) + public Task GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion) { if (info == null) { @@ -1844,7 +1846,7 @@ namespace Emby.Server.Implementations.Session } /// - public SessionInfo GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint) + public Task GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint) { var items = _authRepo.Get(new AuthenticationInfoQuery { diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index 39c369a01d..54e64cfebc 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -97,7 +97,7 @@ namespace Emby.Server.Implementations.Session /// public async Task ProcessWebSocketConnectedAsync(IWebSocketConnection connection) { - var session = GetSession(connection.QueryString, connection.RemoteEndPoint.ToString()); + var session = await GetSession(connection.QueryString, connection.RemoteEndPoint.ToString()).ConfigureAwait(false); if (session != null) { EnsureController(session, connection); @@ -109,7 +109,7 @@ namespace Emby.Server.Implementations.Session } } - private SessionInfo GetSession(IQueryCollection queryString, string remoteEndpoint) + private Task GetSession(IQueryCollection queryString, string remoteEndpoint) { if (queryString == null) { diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 3ca6488d91..99f8ede3ae 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -86,9 +86,9 @@ namespace Jellyfin.Api.Controllers [HttpGet("Options")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetDeviceOptions([FromQuery, Required] string id) + public async Task> GetDeviceOptions([FromQuery, Required] string id) { - var deviceInfo = _deviceManager.GetDeviceOptions(id); + var deviceInfo = await _deviceManager.GetDeviceOptions(id).ConfigureAwait(false); if (deviceInfo == null) { return NotFound(); diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 24ee833ef7..3e9b8bfa4d 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -429,10 +429,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("Tuners/{tunerId}/Reset")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.DefaultAuthorization)] - public ActionResult ResetTuner([FromRoute, Required] string tunerId) + public async Task ResetTuner([FromRoute, Required] string tunerId) { - AssertUserCanManageLiveTv(); - _liveTvManager.ResetTuner(tunerId, CancellationToken.None); + await AssertUserCanManageLiveTv().ConfigureAwait(false); + await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -761,9 +761,9 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId) + public async Task DeleteRecording([FromRoute, Required] Guid recordingId) { - AssertUserCanManageLiveTv(); + await AssertUserCanManageLiveTv().ConfigureAwait(false); var item = _libraryManager.GetItemById(recordingId); if (item == null) @@ -790,7 +790,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task CancelTimer([FromRoute, Required] string timerId) { - AssertUserCanManageLiveTv(); + await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false); return NoContent(); } @@ -808,7 +808,7 @@ namespace Jellyfin.Api.Controllers [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")] public async Task UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo) { - AssertUserCanManageLiveTv(); + await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -824,7 +824,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task CreateTimer([FromBody] TimerInfoDto timerInfo) { - AssertUserCanManageLiveTv(); + await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -882,7 +882,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task CancelSeriesTimer([FromRoute, Required] string timerId) { - AssertUserCanManageLiveTv(); + await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false); return NoContent(); } @@ -900,7 +900,7 @@ namespace Jellyfin.Api.Controllers [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")] public async Task UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo) { - AssertUserCanManageLiveTv(); + await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -916,7 +916,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo) { - AssertUserCanManageLiveTv(); + await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -1215,9 +1215,9 @@ namespace Jellyfin.Api.Controllers return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container)); } - private void AssertUserCanManageLiveTv() + private async Task AssertUserCanManageLiveTv() { - var user = _sessionContext.GetUser(Request); + var user = await _sessionContext.GetUser(Request).ConfigureAwait(false); if (user == null) { diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs index f256c8c25c..cc8c630b35 100644 --- a/Jellyfin.Api/Controllers/PlaystateController.cs +++ b/Jellyfin.Api/Controllers/PlaystateController.cs @@ -72,13 +72,13 @@ namespace Jellyfin.Api.Controllers /// An containing the . [HttpPost("Users/{userId}/PlayedItems/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult MarkPlayedItem( + public async Task> MarkPlayedItem( [FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId, [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed) { var user = _userManager.GetUserById(userId); - var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request); + var session = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false); var dto = UpdatePlayedStatus(user, itemId, true, datePlayed); foreach (var additionalUserInfo in session.AdditionalUsers) { @@ -98,10 +98,10 @@ namespace Jellyfin.Api.Controllers /// A containing the . [HttpDelete("Users/{userId}/PlayedItems/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) + public async Task> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { var user = _userManager.GetUserById(userId); - var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request); + var session = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false); var dto = UpdatePlayedStatus(user, itemId, false, null); foreach (var additionalUserInfo in session.AdditionalUsers) { @@ -123,7 +123,7 @@ namespace Jellyfin.Api.Controllers public async Task ReportPlaybackStart([FromBody] PlaybackStartInfo playbackStartInfo) { playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId); - playbackStartInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false); return NoContent(); } @@ -139,7 +139,7 @@ namespace Jellyfin.Api.Controllers public async Task ReportPlaybackProgress([FromBody] PlaybackProgressInfo playbackProgressInfo) { playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId); - playbackProgressInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false); return NoContent(); } @@ -174,7 +174,7 @@ namespace Jellyfin.Api.Controllers await _transcodingJobHelper.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); } - playbackStopInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false); return NoContent(); } @@ -220,7 +220,7 @@ namespace Jellyfin.Api.Controllers }; playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId); - playbackStartInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false); return NoContent(); } @@ -278,7 +278,7 @@ namespace Jellyfin.Api.Controllers }; playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId); - playbackProgressInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false); return NoContent(); } @@ -323,7 +323,7 @@ namespace Jellyfin.Api.Controllers await _transcodingJobHelper.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); } - playbackStopInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index e2269a2ce2..14ce75514b 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; @@ -124,7 +125,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Sessions/{sessionId}/Viewing")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult DisplayContent( + public async Task DisplayContent( [FromRoute, Required] string sessionId, [FromQuery, Required] string itemType, [FromQuery, Required] string itemId, @@ -137,11 +138,12 @@ namespace Jellyfin.Api.Controllers ItemType = itemType }; - _sessionManager.SendBrowseCommand( - RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, + await _sessionManager.SendBrowseCommand( + await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false), sessionId, command, - CancellationToken.None); + CancellationToken.None) + .ConfigureAwait(false); return NoContent(); } @@ -158,7 +160,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Sessions/{sessionId}/Playing")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult Play( + public async Task Play( [FromRoute, Required] string sessionId, [FromQuery, Required] PlayCommand playCommand, [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds, @@ -171,11 +173,12 @@ namespace Jellyfin.Api.Controllers PlayCommand = playCommand }; - _sessionManager.SendPlayCommand( - RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, + await _sessionManager.SendPlayCommand( + await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false), sessionId, playRequest, - CancellationToken.None); + CancellationToken.None) + .ConfigureAwait(false); return NoContent(); } @@ -192,14 +195,14 @@ namespace Jellyfin.Api.Controllers [HttpPost("Sessions/{sessionId}/Playing/{command}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult SendPlaystateCommand( + public async Task SendPlaystateCommand( [FromRoute, Required] string sessionId, [FromRoute, Required] PlaystateCommand command, [FromQuery] long? seekPositionTicks, [FromQuery] string? controllingUserId) { - _sessionManager.SendPlaystateCommand( - RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, + await _sessionManager.SendPlaystateCommand( + await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false), sessionId, new PlaystateRequest() { @@ -207,7 +210,8 @@ namespace Jellyfin.Api.Controllers ControllingUserId = controllingUserId, SeekPositionTicks = seekPositionTicks, }, - CancellationToken.None); + CancellationToken.None) + .ConfigureAwait(false); return NoContent(); } @@ -222,18 +226,18 @@ namespace Jellyfin.Api.Controllers [HttpPost("Sessions/{sessionId}/System/{command}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult SendSystemCommand( + public async Task SendSystemCommand( [FromRoute, Required] string sessionId, [FromRoute, Required] GeneralCommandType command) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false); var generalCommand = new GeneralCommand { Name = command, ControllingUserId = currentSession.UserId }; - _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None); + await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None); return NoContent(); } @@ -248,11 +252,11 @@ namespace Jellyfin.Api.Controllers [HttpPost("Sessions/{sessionId}/Command/{command}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult SendGeneralCommand( + public async Task SendGeneralCommand( [FromRoute, Required] string sessionId, [FromRoute, Required] GeneralCommandType command) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false); var generalCommand = new GeneralCommand { @@ -260,7 +264,8 @@ namespace Jellyfin.Api.Controllers ControllingUserId = currentSession.UserId }; - _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None); + await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None) + .ConfigureAwait(false); return NoContent(); } @@ -275,11 +280,12 @@ namespace Jellyfin.Api.Controllers [HttpPost("Sessions/{sessionId}/Command")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult SendFullGeneralCommand( + public async Task SendFullGeneralCommand( [FromRoute, Required] string sessionId, [FromBody, Required] GeneralCommand command) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authContext, Request) + .ConfigureAwait(false); if (command == null) { @@ -288,11 +294,12 @@ namespace Jellyfin.Api.Controllers command.ControllingUserId = currentSession.UserId; - _sessionManager.SendGeneralCommand( + await _sessionManager.SendGeneralCommand( currentSession.Id, sessionId, command, - CancellationToken.None); + CancellationToken.None) + .ConfigureAwait(false); return NoContent(); } @@ -309,7 +316,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Sessions/{sessionId}/Message")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult SendMessageCommand( + public async Task SendMessageCommand( [FromRoute, Required] string sessionId, [FromQuery, Required] string text, [FromQuery] string? header, @@ -322,7 +329,12 @@ namespace Jellyfin.Api.Controllers Text = text }; - _sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, sessionId, command, CancellationToken.None); + await _sessionManager.SendMessageCommand( + await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false), + sessionId, + command, + CancellationToken.None) + .ConfigureAwait(false); return NoContent(); } @@ -377,7 +389,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Sessions/Capabilities")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult PostCapabilities( + public async Task PostCapabilities( [FromQuery] string? id, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands, @@ -387,7 +399,7 @@ namespace Jellyfin.Api.Controllers { if (string.IsNullOrWhiteSpace(id)) { - id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + id = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); } _sessionManager.ReportCapabilities(id, new ClientCapabilities @@ -411,13 +423,13 @@ namespace Jellyfin.Api.Controllers [HttpPost("Sessions/Capabilities/Full")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult PostFullCapabilities( + public async Task PostFullCapabilities( [FromQuery] string? id, [FromBody, Required] ClientCapabilitiesDto capabilities) { if (string.IsNullOrWhiteSpace(id)) { - id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + id = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); } _sessionManager.ReportCapabilities(id, capabilities.ToClientCapabilities()); @@ -435,11 +447,11 @@ namespace Jellyfin.Api.Controllers [HttpPost("Sessions/Viewing")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult ReportViewing( + public async Task ReportViewing( [FromQuery] string? sessionId, [FromQuery, Required] string? itemId) { - string session = sessionId ?? RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + string session = sessionId ?? await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); _sessionManager.ReportNowViewingItem(session, itemId); return NoContent(); diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs index f878f2329c..1b3248c0c6 100644 --- a/Jellyfin.Api/Controllers/SyncPlayController.cs +++ b/Jellyfin.Api/Controllers/SyncPlayController.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Threading; +using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.SyncPlayDtos; @@ -51,10 +52,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("New")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayCreateGroup)] - public ActionResult SyncPlayCreateGroup( + public async Task SyncPlayCreateGroup( [FromBody, Required] NewGroupRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new NewGroupRequest(requestData.GroupName); _syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -69,10 +70,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("Join")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayJoinGroup)] - public ActionResult SyncPlayJoinGroup( + public async Task SyncPlayJoinGroup( [FromBody, Required] JoinGroupRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new JoinGroupRequest(requestData.GroupId); _syncPlayManager.JoinGroup(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -86,9 +87,9 @@ namespace Jellyfin.Api.Controllers [HttpPost("Leave")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlayLeaveGroup() + public async Task SyncPlayLeaveGroup() { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new LeaveGroupRequest(); _syncPlayManager.LeaveGroup(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -102,9 +103,9 @@ namespace Jellyfin.Api.Controllers [HttpGet("List")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.SyncPlayJoinGroup)] - public ActionResult> SyncPlayGetGroups() + public async Task>> SyncPlayGetGroups() { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new ListGroupsRequest(); return Ok(_syncPlayManager.ListGroups(currentSession, syncPlayRequest)); } @@ -118,10 +119,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("SetNewQueue")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlaySetNewQueue( + public async Task SyncPlaySetNewQueue( [FromBody, Required] PlayRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new PlayGroupRequest( requestData.PlayingQueue, requestData.PlayingItemPosition, @@ -139,10 +140,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("SetPlaylistItem")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlaySetPlaylistItem( + public async Task SyncPlaySetPlaylistItem( [FromBody, Required] SetPlaylistItemRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new SetPlaylistItemGroupRequest(requestData.PlaylistItemId); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -157,10 +158,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("RemoveFromPlaylist")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlayRemoveFromPlaylist( + public async Task SyncPlayRemoveFromPlaylist( [FromBody, Required] RemoveFromPlaylistRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new RemoveFromPlaylistGroupRequest(requestData.PlaylistItemIds); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -175,10 +176,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("MovePlaylistItem")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlayMovePlaylistItem( + public async Task SyncPlayMovePlaylistItem( [FromBody, Required] MovePlaylistItemRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new MovePlaylistItemGroupRequest(requestData.PlaylistItemId, requestData.NewIndex); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -193,10 +194,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("Queue")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlayQueue( + public async Task SyncPlayQueue( [FromBody, Required] QueueRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new QueueGroupRequest(requestData.ItemIds, requestData.Mode); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -210,9 +211,9 @@ namespace Jellyfin.Api.Controllers [HttpPost("Unpause")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlayUnpause() + public async Task SyncPlayUnpause() { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new UnpauseGroupRequest(); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -226,9 +227,9 @@ namespace Jellyfin.Api.Controllers [HttpPost("Pause")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlayPause() + public async Task SyncPlayPause() { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new PauseGroupRequest(); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -242,9 +243,9 @@ namespace Jellyfin.Api.Controllers [HttpPost("Stop")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlayStop() + public async Task SyncPlayStop() { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new StopGroupRequest(); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -259,10 +260,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("Seek")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlaySeek( + public async Task SyncPlaySeek( [FromBody, Required] SeekRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new SeekGroupRequest(requestData.PositionTicks); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -277,10 +278,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("Buffering")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlayBuffering( + public async Task SyncPlayBuffering( [FromBody, Required] BufferRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new BufferGroupRequest( requestData.When, requestData.PositionTicks, @@ -299,10 +300,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("Ready")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlayReady( + public async Task SyncPlayReady( [FromBody, Required] ReadyRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new ReadyGroupRequest( requestData.When, requestData.PositionTicks, @@ -321,10 +322,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("SetIgnoreWait")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlaySetIgnoreWait( + public async Task SyncPlaySetIgnoreWait( [FromBody, Required] IgnoreWaitRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new IgnoreWaitGroupRequest(requestData.IgnoreWait); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -339,10 +340,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("NextItem")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlayNextItem( + public async Task SyncPlayNextItem( [FromBody, Required] NextItemRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new NextItemGroupRequest(requestData.PlaylistItemId); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -357,10 +358,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("PreviousItem")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlayPreviousItem( + public async Task SyncPlayPreviousItem( [FromBody, Required] PreviousItemRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new PreviousItemGroupRequest(requestData.PlaylistItemId); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -375,10 +376,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("SetRepeatMode")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlaySetRepeatMode( + public async Task SyncPlaySetRepeatMode( [FromBody, Required] SetRepeatModeRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new SetRepeatModeGroupRequest(requestData.Mode); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -393,10 +394,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("SetShuffleMode")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlaySetShuffleMode( + public async Task SyncPlaySetShuffleMode( [FromBody, Required] SetShuffleModeRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new SetShuffleModeGroupRequest(requestData.Mode); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -410,10 +411,10 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("Ping")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult SyncPlayPing( + public async Task SyncPlayPing( [FromBody, Required] PingRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new PingGroupRequest(requestData.Ping); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 94856e03ed..72b60ee570 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; @@ -75,17 +76,17 @@ namespace Jellyfin.Api.Helpers return true; } - internal static SessionInfo GetSession(ISessionManager sessionManager, IAuthorizationContext authContext, HttpRequest request) + internal static async Task GetSession(ISessionManager sessionManager, IAuthorizationContext authContext, HttpRequest request) { var authorization = authContext.GetAuthorizationInfo(request); var user = authorization.User; - var session = sessionManager.LogSessionActivity( + var session = await sessionManager.LogSessionActivity( authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, request.HttpContext.GetNormalizedRemoteIp(), - user); + user).ConfigureAwait(false); if (session == null) { @@ -95,6 +96,13 @@ namespace Jellyfin.Api.Helpers return session; } + internal static async Task GetSessionId(ISessionManager sessionManager, IAuthorizationContext authContext, HttpRequest request) + { + var session = await GetSession(sessionManager, authContext, request).ConfigureAwait(false); + + return session.Id; + } + internal static QueryResult CreateQueryResult( QueryResult<(BaseItem, ItemCounts)> result, DtoOptions dtoOptions, diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index c942678d97..0d93ee2bf1 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -53,12 +53,13 @@ namespace Jellyfin.Server.Implementations.Devices } /// - public DeviceOptions? GetDeviceOptions(string deviceId) + public async Task GetDeviceOptions(string deviceId) { - using var dbContext = _dbProvider.CreateContext(); - return dbContext.DeviceOptions + await using var dbContext = _dbProvider.CreateContext(); + return await dbContext.DeviceOptions .AsQueryable() - .FirstOrDefault(d => d.DeviceId == deviceId); + .FirstOrDefaultAsync(d => d.DeviceId == deviceId) + .ConfigureAwait(false); } /// diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index aa05ead8fd..0df0407942 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -50,6 +50,6 @@ namespace MediaBrowser.Controller.Devices Task UpdateDeviceOptions(string deviceId, DeviceOptions options); - DeviceOptions GetDeviceOptions(string deviceId); + Task GetDeviceOptions(string deviceId); } } diff --git a/MediaBrowser.Controller/Net/ISessionContext.cs b/MediaBrowser.Controller/Net/ISessionContext.cs index a60dc2ea19..57938da1f4 100644 --- a/MediaBrowser.Controller/Net/ISessionContext.cs +++ b/MediaBrowser.Controller/Net/ISessionContext.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Session; using Microsoft.AspNetCore.Http; @@ -8,12 +9,12 @@ namespace MediaBrowser.Controller.Net { public interface ISessionContext { - SessionInfo GetSession(object requestContext); + Task GetSession(object requestContext); - User GetUser(object requestContext); + Task GetUser(object requestContext); - SessionInfo GetSession(HttpContext requestContext); + Task GetSession(HttpContext requestContext); - User GetUser(HttpContext requestContext); + Task GetUser(HttpContext requestContext); } } diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 9eb486534c..630037570e 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -80,7 +80,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, Jellyfin.Data.Entities.User user); + Task LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, Jellyfin.Data.Entities.User user); /// /// Used to report that a session controller has connected. @@ -313,7 +313,7 @@ namespace MediaBrowser.Controller.Session /// The device identifier. /// The remote endpoint. /// SessionInfo. - SessionInfo GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint); + Task GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint); /// /// Gets the session by authentication token. @@ -323,7 +323,7 @@ namespace MediaBrowser.Controller.Session /// The remote endpoint. /// The application version. /// Task<SessionInfo>. - SessionInfo GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion); + Task GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion); /// /// Logouts the specified access token. From 3ebc0474343eb07defb6dd6f0e8bed707471e0a0 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sat, 10 Apr 2021 16:59:41 -0400 Subject: [PATCH 11/82] Convert UpdateUser to solely async --- .../LiveTv/Listings/SchedulesDirect.cs | 4 ++-- .../Session/SessionManager.cs | 2 +- .../Users/UserManager.cs | 17 ++++------------- MediaBrowser.Controller/Library/IUserManager.cs | 16 ++++------------ 4 files changed, 11 insertions(+), 28 deletions(-) diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 1926e738f2..dd58ca8997 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -457,10 +457,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings } StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13)); - foreach (ReadOnlySpan i in programIds) + foreach (string i in programIds) { str.Append('"') - .Append(i.Slice(0, 10)) + .Append(i[..10]) .Append("\","); } diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 678a27665b..50156d2b6f 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -293,7 +293,7 @@ namespace Emby.Server.Implementations.Session try { user.LastActivityDate = activityDate; - _userManager.UpdateUser(user); + await _userManager.UpdateUserAsync(user); } catch (DbUpdateConcurrencyException e) { diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index b6b45e69b5..4d847ec959 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -163,15 +163,6 @@ namespace Jellyfin.Server.Implementations.Users OnUserUpdated?.Invoke(this, new GenericEventArgs(user)); } - /// - public void UpdateUser(User user) - { - using var dbContext = _dbProvider.CreateContext(); - dbContext.Users.Update(user); - _users[user.Id] = user; - dbContext.SaveChanges(); - } - /// public async Task UpdateUserAsync(User user) { @@ -271,9 +262,9 @@ namespace Jellyfin.Server.Implementations.Users } /// - public void ResetEasyPassword(User user) + public Task ResetEasyPassword(User user) { - ChangeEasyPassword(user, string.Empty, null); + return ChangeEasyPassword(user, string.Empty, null); } /// @@ -291,7 +282,7 @@ namespace Jellyfin.Server.Implementations.Users } /// - public void ChangeEasyPassword(User user, string newPassword, string? newPasswordSha1) + public async Task ChangeEasyPassword(User user, string newPassword, string? newPasswordSha1) { if (newPassword != null) { @@ -304,7 +295,7 @@ namespace Jellyfin.Server.Implementations.Users } user.EasyPassword = newPasswordSha1; - UpdateUser(user); + await UpdateUserAsync(user); _eventManager.Publish(new UserPasswordChangedEventArgs(user)); } diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index 6e267834b0..41dfe1b967 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -63,14 +63,6 @@ namespace MediaBrowser.Controller.Library /// Task RenameUser(User user, string newName); - /// - /// Updates the user. - /// - /// The user. - /// user - /// - void UpdateUser(User user); - /// /// Updates the user. /// @@ -108,7 +100,7 @@ namespace MediaBrowser.Controller.Library /// /// The user. /// Task. - void ResetEasyPassword(User user); + Task ResetEasyPassword(User user); /// /// Changes the password. @@ -118,7 +110,7 @@ namespace MediaBrowser.Controller.Library /// /// Changes the easy password. /// - void ChangeEasyPassword(User user, string newPassword, string newPasswordSha1); + Task ChangeEasyPassword(User user, string newPassword, string newPasswordSha1); /// /// Gets the user dto. @@ -155,7 +147,7 @@ namespace MediaBrowser.Controller.Library /// /// 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 directly, then call . + /// Instead, modify the user object directly, then call . /// /// The user's Id. /// The request containing the new user configuration. @@ -165,7 +157,7 @@ namespace MediaBrowser.Controller.Library /// /// 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 directly, then call . + /// Instead, modify the user object directly, then call . /// /// The user's Id. /// The request containing the new user policy. From ed0b5ff0171e340544702d99a07c149e01c5bf8a Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sat, 10 Apr 2021 17:11:59 -0400 Subject: [PATCH 12/82] Fix builds --- Jellyfin.Data/Entities/Security/Device.cs | 5 +++++ Jellyfin.Server.Implementations/Devices/DeviceManager.cs | 9 ++++++--- .../Security/AuthenticationManager.cs | 7 ++++--- Jellyfin.Server.Implementations/Users/UserManager.cs | 2 +- MediaBrowser.Controller/Devices/IDeviceManager.cs | 8 ++++---- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Jellyfin.Data/Entities/Security/Device.cs b/Jellyfin.Data/Entities/Security/Device.cs index 17d17f5946..bb192e7729 100644 --- a/Jellyfin.Data/Entities/Security/Device.cs +++ b/Jellyfin.Data/Entities/Security/Device.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace Jellyfin.Data.Entities.Security { @@ -31,6 +32,10 @@ namespace Jellyfin.Data.Entities.Security User = null!; } + /// + /// Gets the id. + /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; private set; } /// diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index 0d93ee2bf1..4758f24f3b 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -15,6 +15,9 @@ using Microsoft.EntityFrameworkCore; namespace Jellyfin.Server.Implementations.Devices { + /// + /// Manages the creation, updating, and retrieval of devices. + /// public class DeviceManager : IDeviceManager { private readonly JellyfinDbProvider _dbProvider; @@ -63,9 +66,9 @@ namespace Jellyfin.Server.Implementations.Devices } /// - public ClientCapabilities GetCapabilities(string id) + public ClientCapabilities GetCapabilities(string deviceId) { - return _capabilitiesMap.TryGetValue(id, out ClientCapabilities? result) + return _capabilitiesMap.TryGetValue(deviceId, out ClientCapabilities? result) ? result : new ClientCapabilities(); } @@ -112,7 +115,7 @@ namespace Jellyfin.Server.Implementations.Devices sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId)); } - var array = await sessions.Select(ToDeviceInfo).ToArrayAsync(); + var array = await sessions.Select(ToDeviceInfo).ToArrayAsync().ConfigureAwait(false); return new QueryResult(array); } diff --git a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs index 37b8ee6e03..ab76e2302f 100644 --- a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs +++ b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs @@ -52,14 +52,15 @@ namespace Jellyfin.Server.Implementations.Security } /// - public async Task DeleteApiKey(Guid id) + public async Task DeleteApiKey(Guid accessToken) { await using var dbContext = _dbProvider.CreateContext(); var key = await dbContext.ApiKeys .AsQueryable() - .Where(apiKey => apiKey.AccessToken == id) - .FirstOrDefaultAsync(); + .Where(apiKey => apiKey.AccessToken == accessToken) + .FirstOrDefaultAsync() + .ConfigureAwait(false); if (key == null) { diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 4d847ec959..87d33330f6 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -295,7 +295,7 @@ namespace Jellyfin.Server.Implementations.Users } user.EasyPassword = newPasswordSha1; - await UpdateUserAsync(user); + await UpdateUserAsync(user).ConfigureAwait(false); _eventManager.Publish(new UserPasswordChangedEventArgs(user)); } diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index 0df0407942..4cdd8575e1 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -18,16 +18,16 @@ namespace MediaBrowser.Controller.Devices /// /// Saves the capabilities. /// - /// The reported identifier. + /// The device id. /// The capabilities. - void SaveCapabilities(string reportedId, ClientCapabilities capabilities); + void SaveCapabilities(string deviceId, ClientCapabilities capabilities); /// /// Gets the capabilities. /// - /// The reported identifier. + /// The device id. /// ClientCapabilities. - ClientCapabilities GetCapabilities(string reportedId); + ClientCapabilities GetCapabilities(string deviceId); /// /// Gets the device information. From 75df6965a046967dca94912c9490fd4b4af9f0a6 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 13 Apr 2021 20:01:21 -0400 Subject: [PATCH 13/82] Don't use database for QuickConnect --- .../QuickConnect/QuickConnectManager.cs | 71 ++++++++----------- .../Session/SessionManager.cs | 18 ----- .../QuickConnect/IQuickConnect.cs | 8 +++ 3 files changed, 38 insertions(+), 59 deletions(-) diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs index 7bed06de36..9f639138a9 100644 --- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs +++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs @@ -1,16 +1,15 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Security.Cryptography; -using MediaBrowser.Common; using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.QuickConnect; -using MediaBrowser.Controller.Security; +using MediaBrowser.Controller.Session; using MediaBrowser.Model.QuickConnect; using Microsoft.Extensions.Logging; @@ -21,36 +20,26 @@ namespace Emby.Server.Implementations.QuickConnect /// public class QuickConnectManager : IQuickConnect, IDisposable { - private readonly RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider(); - private readonly ConcurrentDictionary _currentRequests = new ConcurrentDictionary(); + private readonly RNGCryptoServiceProvider _rng = new (); + private readonly ConcurrentDictionary _currentRequests = new (); + private readonly ConcurrentDictionary _quickConnectTokens = new (); private readonly IServerConfigurationManager _config; private readonly ILogger _logger; - private readonly IAuthenticationRepository _authenticationRepository; - private readonly IAuthorizationContext _authContext; - private readonly IServerApplicationHost _appHost; + private readonly ISessionManager _sessionManager; /// /// Initializes a new instance of the class. /// Should only be called at server startup when a singleton is created. /// - /// Configuration. - /// Logger. - /// Application host. - /// Authentication context. - /// Authentication repository. - public QuickConnectManager( - IServerConfigurationManager config, - ILogger logger, - IServerApplicationHost appHost, - IAuthorizationContext authContext, - IAuthenticationRepository authenticationRepository) + /// The server configuration manager. + /// The logger. + /// The session manager. + public QuickConnectManager(IServerConfigurationManager config, ILogger logger, ISessionManager sessionManager) { _config = config; _logger = logger; - _appHost = appHost; - _authContext = authContext; - _authenticationRepository = authenticationRepository; + _sessionManager = sessionManager; ReloadConfiguration(); } @@ -138,6 +127,19 @@ namespace Emby.Server.Implementations.QuickConnect return result; } + public void AuthenticateRequest(AuthenticationRequest request, string token) + { + if (!_quickConnectTokens.TryGetValue(token, out var entry)) + { + throw new SecurityException("Unknown quick connect token"); + } + + request.UserId = entry.Item2; + _quickConnectTokens.Remove(token, out _); + + _sessionManager.AuthenticateQuickConnect(request, token); + } + /// public string GenerateCode() { @@ -179,16 +181,7 @@ namespace Emby.Server.Implementations.QuickConnect var added = result.DateAdded ?? DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(Timeout)); result.DateAdded = added.Subtract(TimeSpan.FromMinutes(Timeout - 1)); - _authenticationRepository.Create(new AuthenticationInfo - { - AppName = TokenName, - AccessToken = result.Authentication, - DateCreated = DateTime.UtcNow, - DeviceId = _appHost.SystemId, - DeviceName = _appHost.FriendlyName, - AppVersion = _appHost.ApplicationVersionString, - UserId = userId - }); + _quickConnectTokens[result.Authentication] = (TokenName, userId); _logger.LogDebug("Authorizing device with code {Code} to login as user {userId}", code, userId); @@ -198,19 +191,15 @@ namespace Emby.Server.Implementations.QuickConnect /// 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(TokenName, StringComparison.Ordinal)); + var tokens = _quickConnectTokens + .Where(entry => entry.Value.Item1.StartsWith(TokenName, StringComparison.Ordinal) && entry.Value.Item2 == user) + .ToList(); var removed = 0; foreach (var token in tokens) { - _authenticationRepository.Delete(token); - _logger.LogDebug("Deleted token {AccessToken}", token.AccessToken); + _quickConnectTokens.Remove(token.Key, out _); + _logger.LogDebug("Deleted token {AccessToken}", token.Key); removed++; } diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 50156d2b6f..99d2947f0e 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1441,24 +1441,6 @@ namespace Emby.Server.Implementations.Session public Task AuthenticateQuickConnect(AuthenticationRequest request, string token) { - var result = _authRepo.Get(new AuthenticationInfoQuery() - { - AccessToken = token, - DeviceId = _appHost.SystemId, - Limit = 1 - }); - - if (result.TotalRecordCount == 0) - { - throw new SecurityException("Unknown quick connect token"); - } - - var info = result.Items[0]; - request.UserId = info.UserId; - - // There's no need to keep the quick connect token in the database, as AuthenticateNewSessionInternal() issues a long lived token. - _authRepo.Delete(info); - return AuthenticateNewSessionInternal(request, false); } diff --git a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs index 959a2d7712..ce8999e490 100644 --- a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs +++ b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs @@ -1,4 +1,5 @@ using System; +using MediaBrowser.Controller.Session; using MediaBrowser.Model.QuickConnect; namespace MediaBrowser.Controller.QuickConnect @@ -57,6 +58,13 @@ namespace MediaBrowser.Controller.QuickConnect /// Quick connect result. QuickConnectResult CheckRequestStatus(string secret); + /// + /// Authenticates a QuickConnect request. + /// + /// The request. + /// The token. + void AuthenticateRequest(AuthenticationRequest request, string token); + /// /// Authorizes a quick connect request to connect as the calling user. /// From 12fa5c0c41c255f4d74d56eacaa38ed2da420d6f Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 13 Apr 2021 20:01:31 -0400 Subject: [PATCH 14/82] Add AccessToken to device --- Jellyfin.Data/Entities/Security/Device.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Data/Entities/Security/Device.cs b/Jellyfin.Data/Entities/Security/Device.cs index bb192e7729..9bff7f0cff 100644 --- a/Jellyfin.Data/Entities/Security/Device.cs +++ b/Jellyfin.Data/Entities/Security/Device.cs @@ -13,13 +13,15 @@ namespace Jellyfin.Data.Entities.Security /// Initializes a new instance of the class. /// /// The user id. + /// The access token. /// The app name. /// The app version. /// The device name. /// The device id. - public Device(Guid userId, string appName, string appVersion, string deviceName, string deviceId) + public Device(Guid userId, string accessToken, string appName, string appVersion, string deviceName, string deviceId) { UserId = userId; + AccessToken = accessToken; AppName = appName; AppVersion = appVersion; DeviceName = deviceName; @@ -43,6 +45,11 @@ namespace Jellyfin.Data.Entities.Security /// public Guid UserId { get; private set; } + /// + /// Gets or sets the access token. + /// + public string AccessToken { get; set; } + /// /// Gets or sets the app name. /// From e1f70860778687703fcc0e950fb1496afa22775e Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Thu, 20 May 2021 20:39:22 -0400 Subject: [PATCH 15/82] Remove unnecessary query class --- Jellyfin.Api/Controllers/DevicesController.cs | 3 +-- .../Devices/DeviceManager.cs | 13 +++++------- .../Devices/IDeviceManager.cs | 5 +++-- MediaBrowser.Model/Devices/DeviceQuery.cs | 21 ------------------- 4 files changed, 9 insertions(+), 33 deletions(-) delete mode 100644 MediaBrowser.Model/Devices/DeviceQuery.cs diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 99f8ede3ae..4cfae568ad 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -51,8 +51,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) { - var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty }; - return await _deviceManager.GetDevices(deviceQuery); + return await _deviceManager.GetDevicesForUser(userId, supportsSync); } /// diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index 4758f24f3b..bde8eb86ee 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -91,7 +91,7 @@ namespace Jellyfin.Server.Implementations.Devices } /// - public async Task> GetDevices(DeviceQuery query) + public async Task> GetDevicesForUser(Guid? userId, bool? supportsSync) { await using var dbContext = _dbProvider.CreateContext(); var sessions = dbContext.Devices @@ -100,17 +100,14 @@ namespace Jellyfin.Server.Implementations.Devices .ThenByDescending(d => d.DateLastActivity) .AsAsyncEnumerable(); - // TODO: DeviceQuery doesn't seem to be used from client. Not even Swagger. - if (query.SupportsSync.HasValue) + if (supportsSync.HasValue) { - var val = query.SupportsSync.Value; - - sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == val); + sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == supportsSync.Value); } - if (!query.UserId.Equals(Guid.Empty)) + if (userId.HasValue) { - var user = _userManager.GetUserById(query.UserId); + var user = _userManager.GetUserById(userId.Value); sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId)); } diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index 26afd93943..28612cea33 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -41,9 +41,10 @@ namespace MediaBrowser.Controller.Devices /// /// Gets the devices. /// - /// The query. + /// The user's id, or null. + /// A value indicating whether the device supports sync, or null. /// IEnumerable<DeviceInfo>. - Task> GetDevices(DeviceQuery query); + Task> GetDevicesForUser(Guid? userId, bool? supportsSync); /// /// Determines whether this instance [can access device] the specified user identifier. diff --git a/MediaBrowser.Model/Devices/DeviceQuery.cs b/MediaBrowser.Model/Devices/DeviceQuery.cs deleted file mode 100644 index 64e366a560..0000000000 --- a/MediaBrowser.Model/Devices/DeviceQuery.cs +++ /dev/null @@ -1,21 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace MediaBrowser.Model.Devices -{ - public class DeviceQuery - { - /// - /// Gets or sets a value indicating whether [supports synchronize]. - /// - /// null if [supports synchronize] contains no value, true if [supports synchronize]; otherwise, false. - public bool? SupportsSync { get; set; } - - /// - /// Gets or sets the user identifier. - /// - /// The user identifier. - public Guid UserId { get; set; } - } -} From ab63a7745ce43ddab8ab48dd04076773bebe69cd Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Thu, 20 May 2021 20:48:41 -0400 Subject: [PATCH 16/82] Add PaginatedQuery abstract class, change startIndex to skip --- .../Controllers/ActivityLogController.cs | 2 +- Jellyfin.Data/Queries/ActivityLogQuery.cs | 12 +----------- Jellyfin.Data/Queries/PaginatedQuery.cs | 18 ++++++++++++++++++ .../Activity/ActivityManager.cs | 2 +- 4 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 Jellyfin.Data/Queries/PaginatedQuery.cs diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs index b429cebecb..ae45f647f7 100644 --- a/Jellyfin.Api/Controllers/ActivityLogController.cs +++ b/Jellyfin.Api/Controllers/ActivityLogController.cs @@ -47,7 +47,7 @@ namespace Jellyfin.Api.Controllers { return await _activityManager.GetPagedResultAsync(new ActivityLogQuery { - StartIndex = startIndex, + Skip = startIndex, Limit = limit, MinDate = minDate, HasUserId = hasUserId diff --git a/Jellyfin.Data/Queries/ActivityLogQuery.cs b/Jellyfin.Data/Queries/ActivityLogQuery.cs index 92919d3a51..f1af099d3c 100644 --- a/Jellyfin.Data/Queries/ActivityLogQuery.cs +++ b/Jellyfin.Data/Queries/ActivityLogQuery.cs @@ -5,18 +5,8 @@ namespace Jellyfin.Data.Queries /// /// A class representing a query to the activity logs. /// - public class ActivityLogQuery + public class ActivityLogQuery : PaginatedQuery { - /// - /// Gets or sets the index to start at. - /// - public int? StartIndex { get; set; } - - /// - /// Gets or sets the maximum number of items to include. - /// - public int? Limit { get; set; } - /// /// Gets or sets a value indicating whether to take entries with a user id. /// diff --git a/Jellyfin.Data/Queries/PaginatedQuery.cs b/Jellyfin.Data/Queries/PaginatedQuery.cs new file mode 100644 index 0000000000..58267ebe7f --- /dev/null +++ b/Jellyfin.Data/Queries/PaginatedQuery.cs @@ -0,0 +1,18 @@ +namespace Jellyfin.Data.Queries +{ + /// + /// An abstract class for paginated queries. + /// + public abstract class PaginatedQuery + { + /// + /// Gets or sets the index to start at. + /// + public int? Skip { get; set; } + + /// + /// Gets or sets the maximum number of items to include. + /// + public int? Limit { get; set; } + } +} diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs index 27360afb0a..976bb4570c 100644 --- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -62,7 +62,7 @@ namespace Jellyfin.Server.Implementations.Activity return new QueryResult { Items = await entries - .Skip(query.StartIndex ?? 0) + .Skip(query.Skip ?? 0) .Take(query.Limit ?? 100) .AsAsyncEnumerable() .Select(ConvertToOldModel) From f4d1c3ef7ab46cb37f400ef7688dd328d5d1626e Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Thu, 20 May 2021 20:48:53 -0400 Subject: [PATCH 17/82] Add device query class --- Jellyfin.Data/Queries/DeviceQuery.cs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 Jellyfin.Data/Queries/DeviceQuery.cs diff --git a/Jellyfin.Data/Queries/DeviceQuery.cs b/Jellyfin.Data/Queries/DeviceQuery.cs new file mode 100644 index 0000000000..083e00548d --- /dev/null +++ b/Jellyfin.Data/Queries/DeviceQuery.cs @@ -0,0 +1,25 @@ +using System; + +namespace Jellyfin.Data.Queries +{ + /// + /// A query to retrieve devices. + /// + public class DeviceQuery : PaginatedQuery + { + /// + /// Gets or sets the user id of the device. + /// + public Guid? UserId { get; set; } + + /// + /// Gets or sets the device id. + /// + public string? DeviceId { get; set; } + + /// + /// Gets or sets the access token. + /// + public string? AccessToken { get; set; } + } +} From b03f2353d8bb023e659e735e12bd6230f07ca19c Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Thu, 20 May 2021 20:57:10 -0400 Subject: [PATCH 18/82] Generate Access token in constructor --- Jellyfin.Data/Entities/Security/Device.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Data/Entities/Security/Device.cs b/Jellyfin.Data/Entities/Security/Device.cs index 9bff7f0cff..bb05cf5a4f 100644 --- a/Jellyfin.Data/Entities/Security/Device.cs +++ b/Jellyfin.Data/Entities/Security/Device.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using System.Globalization; namespace Jellyfin.Data.Entities.Security { @@ -13,20 +14,19 @@ namespace Jellyfin.Data.Entities.Security /// Initializes a new instance of the class. /// /// The user id. - /// The access token. /// The app name. /// The app version. /// The device name. /// The device id. - public Device(Guid userId, string accessToken, string appName, string appVersion, string deviceName, string deviceId) + public Device(Guid userId, string appName, string appVersion, string deviceName, string deviceId) { UserId = userId; - AccessToken = accessToken; AppName = appName; AppVersion = appVersion; DeviceName = deviceName; DeviceId = deviceId; + AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); DateCreated = DateTime.UtcNow; DateLastActivity = DateCreated; From a0c6f7276211ac0429877fafa400368aba1430a9 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Thu, 20 May 2021 23:56:59 -0400 Subject: [PATCH 19/82] Migrate authentication db to EF Core --- .../ApplicationHost.cs | 10 +- .../HttpServer/Security/AuthService.cs | 5 +- .../HttpServer/Security/SessionContext.cs | 12 +- .../HttpServer/WebSocketManager.cs | 2 +- .../Security/AuthenticationRepository.cs | 407 ----------- .../Session/SessionManager.cs | 80 +-- .../Auth/CustomAuthenticationHandler.cs | 10 +- .../Controllers/CollectionController.cs | 2 +- Jellyfin.Api/Controllers/DevicesController.cs | 18 +- Jellyfin.Api/Controllers/ImageController.cs | 8 +- Jellyfin.Api/Controllers/LibraryController.cs | 10 +- .../Controllers/MediaInfoController.cs | 2 +- .../Controllers/PlaystateController.cs | 6 +- Jellyfin.Api/Controllers/SessionController.cs | 6 +- .../Controllers/SubtitleController.cs | 2 +- .../Controllers/UniversalAudioController.cs | 4 +- Jellyfin.Api/Controllers/UserController.cs | 42 +- .../Controllers/UserViewsController.cs | 5 +- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 2 +- Jellyfin.Api/Helpers/RequestHelpers.cs | 6 +- Jellyfin.Api/Helpers/StreamingHelpers.cs | 4 +- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 2 +- .../Devices/DeviceManager.cs | 73 ++ Jellyfin.Server.Implementations/JellyfinDb.cs | 9 + .../20210521032224_AddDevices.Designer.cs | 645 ++++++++++++++++++ .../Migrations/20210521032224_AddDevices.cs | 126 ++++ .../Migrations/JellyfinDbModelSnapshot.cs | 114 +++- .../Security/AuthorizationContext.cs | 102 ++- .../Users/DeviceAccessEntryPoint.cs | 22 +- Jellyfin.Server/CoreAppHost.cs | 3 + .../Devices/IDeviceManager.cs | 19 + MediaBrowser.Controller/Net/IAuthService.cs | 3 +- .../Net/IAuthorizationContext.cs | 9 +- .../Security/AuthenticationInfoQuery.cs | 53 -- .../Security/IAuthenticationRepository.cs | 39 -- .../Session/ISessionManager.cs | 19 +- MediaBrowser.Model/Devices/DeviceInfo.cs | 5 + .../Auth/CustomAuthenticationHandlerTests.cs | 2 +- 38 files changed, 1172 insertions(+), 716 deletions(-) delete mode 100644 Emby.Server.Implementations/Security/AuthenticationRepository.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20210521032224_AddDevices.Designer.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20210521032224_AddDevices.cs rename {Emby.Server.Implementations/HttpServer => Jellyfin.Server.Implementations}/Security/AuthorizationContext.cs (64%) delete mode 100644 MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs delete mode 100644 MediaBrowser.Controller/Security/IAuthenticationRepository.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 213890c679..a69c4d0353 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -36,7 +36,6 @@ using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.Plugins; using Emby.Server.Implementations.QuickConnect; using Emby.Server.Implementations.ScheduledTasks; -using Emby.Server.Implementations.Security; using Emby.Server.Implementations.Serialization; using Emby.Server.Implementations.Session; using Emby.Server.Implementations.SyncPlay; @@ -57,7 +56,6 @@ using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; @@ -73,7 +71,6 @@ using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.QuickConnect; using MediaBrowser.Controller.Resolvers; -using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Subtitles; @@ -599,8 +596,6 @@ namespace Emby.Server.Implementations ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); ServiceCollection.AddSingleton(); @@ -657,8 +652,7 @@ namespace Emby.Server.Implementations ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); + ServiceCollection.AddScoped(); ServiceCollection.AddSingleton(); ServiceCollection.AddSingleton(); @@ -687,8 +681,6 @@ namespace Emby.Server.Implementations _mediaEncoder = Resolve(); _sessionManager = Resolve(); - ((AuthenticationRepository)Resolve()).Initialize(); - SetStaticProperties(); var userDataRepo = (SqliteUserDataRepository)Resolve(); diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index 9afabf5272..e2ad07177e 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using System.Threading.Tasks; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Net; @@ -17,9 +18,9 @@ namespace Emby.Server.Implementations.HttpServer.Security _authorizationContext = authorizationContext; } - public AuthorizationInfo Authenticate(HttpRequest request) + public async Task Authenticate(HttpRequest request) { - var auth = _authorizationContext.GetAuthorizationInfo(request); + var auth = await _authorizationContext.GetAuthorizationInfo(request).ConfigureAwait(false); if (!auth.HasToken) { diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs index 414ba7ca01..cd1b9cba01 100644 --- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs @@ -24,12 +24,18 @@ namespace Emby.Server.Implementations.HttpServer.Security _sessionManager = sessionManager; } - public Task GetSession(HttpContext requestContext) + public async Task GetSession(HttpContext requestContext) { - var authorization = _authContext.GetAuthorizationInfo(requestContext); + var authorization = await _authContext.GetAuthorizationInfo(requestContext).ConfigureAwait(false); var user = authorization.User; - return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp().ToString(), user); + return await _sessionManager.LogSessionActivity( + authorization.Client, + authorization.Version, + authorization.DeviceId, + authorization.Device, + requestContext.GetNormalizedRemoteIp().ToString(), + user).ConfigureAwait(false); } public Task GetSession(object requestContext) diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs index 1bee1ac310..b71ffdaee5 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs @@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.HttpServer /// public async Task WebSocketRequestHandler(HttpContext context) { - _ = _authService.Authenticate(context.Request); + _ = await _authService.Authenticate(context.Request).ConfigureAwait(false); try { _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress); diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs deleted file mode 100644 index 0d0a2b1df7..0000000000 --- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs +++ /dev/null @@ -1,407 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using Emby.Server.Implementations.Data; -using Jellyfin.Data.Entities.Security; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Security; -using MediaBrowser.Model.Querying; -using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; - -namespace Emby.Server.Implementations.Security -{ - public class AuthenticationRepository : BaseSqliteRepository, IAuthenticationRepository - { - public AuthenticationRepository(ILogger logger, IServerConfigurationManager config) - : base(logger) - { - DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "authentication.db"); - } - - public void Initialize() - { - string[] queries = - { - "create table if not exists Tokens (Id INTEGER PRIMARY KEY, AccessToken TEXT NOT NULL, DeviceId TEXT NOT NULL, AppName TEXT NOT NULL, AppVersion TEXT NOT NULL, DeviceName TEXT NOT NULL, UserId TEXT, UserName TEXT, IsActive BIT NOT NULL, DateCreated DATETIME NOT NULL, DateLastActivity DATETIME NOT NULL)", - "create table if not exists Devices (Id TEXT NOT NULL PRIMARY KEY, CustomName TEXT, Capabilities TEXT)", - "drop index if exists idx_AccessTokens", - "drop index if exists Tokens1", - "drop index if exists Tokens2", - - "create index if not exists Tokens3 on Tokens (AccessToken, DateLastActivity)", - "create index if not exists Tokens4 on Tokens (Id, DateLastActivity)", - "create index if not exists Devices1 on Devices (Id)" - }; - - using (var connection = GetConnection()) - { - var tableNewlyCreated = !TableExists(connection, "Tokens"); - - connection.RunQueries(queries); - - TryMigrate(connection, tableNewlyCreated); - } - } - - private void TryMigrate(ManagedConnection connection, bool tableNewlyCreated) - { - try - { - if (tableNewlyCreated && TableExists(connection, "AccessTokens")) - { - connection.RunInTransaction( - db => - { - var existingColumnNames = GetColumnNames(db, "AccessTokens"); - - AddColumn(db, "AccessTokens", "UserName", "TEXT", existingColumnNames); - AddColumn(db, "AccessTokens", "DateLastActivity", "DATETIME", existingColumnNames); - AddColumn(db, "AccessTokens", "AppVersion", "TEXT", existingColumnNames); - }, TransactionMode); - - connection.RunQueries(new[] - { - "update accesstokens set DateLastActivity=DateCreated where DateLastActivity is null", - "update accesstokens set DeviceName='Unknown' where DeviceName is null", - "update accesstokens set AppName='Unknown' where AppName is null", - "update accesstokens set AppVersion='1' where AppVersion is null", - "INSERT INTO Tokens (AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity) SELECT AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity FROM AccessTokens where deviceid not null and devicename not null and appname not null and isactive=1" - }); - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Error migrating authentication database"); - } - } - - public void Create(AuthenticationInfo info) - { - if (info == null) - { - throw new ArgumentNullException(nameof(info)); - } - - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => - { - using (var statement = db.PrepareStatement("insert into Tokens (AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity) values (@AccessToken, @DeviceId, @AppName, @AppVersion, @DeviceName, @UserId, @UserName, @IsActive, @DateCreated, @DateLastActivity)")) - { - statement.TryBind("@AccessToken", info.AccessToken); - - statement.TryBind("@DeviceId", info.DeviceId); - statement.TryBind("@AppName", info.AppName); - statement.TryBind("@AppVersion", info.AppVersion); - statement.TryBind("@DeviceName", info.DeviceName); - statement.TryBind("@UserId", info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N", CultureInfo.InvariantCulture)); - statement.TryBind("@UserName", info.UserName); - statement.TryBind("@IsActive", true); - statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue()); - statement.TryBind("@DateLastActivity", info.DateLastActivity.ToDateTimeParamValue()); - - statement.MoveNext(); - } - }, TransactionMode); - } - } - - public void Update(AuthenticationInfo info) - { - if (info == null) - { - throw new ArgumentNullException(nameof(info)); - } - - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => - { - using (var statement = db.PrepareStatement("Update Tokens set AccessToken=@AccessToken, DeviceId=@DeviceId, AppName=@AppName, AppVersion=@AppVersion, DeviceName=@DeviceName, UserId=@UserId, UserName=@UserName, DateCreated=@DateCreated, DateLastActivity=@DateLastActivity where Id=@Id")) - { - statement.TryBind("@Id", info.Id); - - statement.TryBind("@AccessToken", info.AccessToken); - - statement.TryBind("@DeviceId", info.DeviceId); - statement.TryBind("@AppName", info.AppName); - statement.TryBind("@AppVersion", info.AppVersion); - statement.TryBind("@DeviceName", info.DeviceName); - statement.TryBind("@UserId", info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N", CultureInfo.InvariantCulture)); - statement.TryBind("@UserName", info.UserName); - statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue()); - statement.TryBind("@DateLastActivity", info.DateLastActivity.ToDateTimeParamValue()); - - statement.MoveNext(); - } - }, TransactionMode); - } - } - - public void Delete(AuthenticationInfo info) - { - if (info == null) - { - throw new ArgumentNullException(nameof(info)); - } - - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => - { - using (var statement = db.PrepareStatement("Delete from Tokens where Id=@Id")) - { - statement.TryBind("@Id", info.Id); - - statement.MoveNext(); - } - }, TransactionMode); - } - } - - private const string BaseSelectText = "select Tokens.Id, AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, DateCreated, DateLastActivity, Devices.CustomName from Tokens left join Devices on Tokens.DeviceId=Devices.Id"; - - private static void BindAuthenticationQueryParams(AuthenticationInfoQuery query, IStatement statement) - { - if (!string.IsNullOrEmpty(query.AccessToken)) - { - statement.TryBind("@AccessToken", query.AccessToken); - } - - if (!query.UserId.Equals(Guid.Empty)) - { - statement.TryBind("@UserId", query.UserId.ToString("N", CultureInfo.InvariantCulture)); - } - - if (!string.IsNullOrEmpty(query.DeviceId)) - { - statement.TryBind("@DeviceId", query.DeviceId); - } - } - - public QueryResult Get(AuthenticationInfoQuery query) - { - if (query == null) - { - throw new ArgumentNullException(nameof(query)); - } - - var commandText = BaseSelectText; - - var whereClauses = new List(); - - if (!string.IsNullOrEmpty(query.AccessToken)) - { - whereClauses.Add("AccessToken=@AccessToken"); - } - - if (!string.IsNullOrEmpty(query.DeviceId)) - { - whereClauses.Add("DeviceId=@DeviceId"); - } - - if (!query.UserId.Equals(Guid.Empty)) - { - whereClauses.Add("UserId=@UserId"); - } - - if (query.HasUser.HasValue) - { - if (query.HasUser.Value) - { - whereClauses.Add("UserId not null"); - } - else - { - whereClauses.Add("UserId is null"); - } - } - - var whereTextWithoutPaging = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - commandText += whereTextWithoutPaging; - - commandText += " ORDER BY DateLastActivity desc"; - - if (query.Limit.HasValue || query.StartIndex.HasValue) - { - var offset = query.StartIndex ?? 0; - - if (query.Limit.HasValue || offset > 0) - { - commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); - } - - if (offset > 0) - { - commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); - } - } - - var statementTexts = new[] - { - commandText, - "select count (Id) from Tokens" + whereTextWithoutPaging - }; - - var list = new List(); - var result = new QueryResult(); - using (var connection = GetConnection(true)) - { - connection.RunInTransaction( - db => - { - var statements = PrepareAll(db, statementTexts); - - using (var statement = statements[0]) - { - BindAuthenticationQueryParams(query, statement); - - foreach (var row in statement.ExecuteQuery()) - { - list.Add(Get(row)); - } - - using (var totalCountStatement = statements[1]) - { - BindAuthenticationQueryParams(query, totalCountStatement); - - result.TotalRecordCount = totalCountStatement.ExecuteQuery() - .SelectScalarInt() - .First(); - } - } - }, - ReadTransactionMode); - } - - result.Items = list; - return result; - } - - private static AuthenticationInfo Get(IReadOnlyList reader) - { - var info = new AuthenticationInfo - { - Id = reader[0].ToInt64(), - AccessToken = reader[1].ToString() - }; - - if (reader[2].SQLiteType != SQLiteType.Null) - { - info.DeviceId = reader[2].ToString(); - } - - if (reader[3].SQLiteType != SQLiteType.Null) - { - info.AppName = reader[3].ToString(); - } - - if (reader[4].SQLiteType != SQLiteType.Null) - { - info.AppVersion = reader[4].ToString(); - } - - if (reader[5].SQLiteType != SQLiteType.Null) - { - info.DeviceName = reader[5].ToString(); - } - - if (reader[6].SQLiteType != SQLiteType.Null) - { - info.UserId = new Guid(reader[6].ToString()); - } - - if (reader[7].SQLiteType != SQLiteType.Null) - { - info.UserName = reader[7].ToString(); - } - - info.DateCreated = reader[8].ReadDateTime(); - - if (reader[9].SQLiteType != SQLiteType.Null) - { - info.DateLastActivity = reader[9].ReadDateTime(); - } - else - { - info.DateLastActivity = info.DateCreated; - } - - if (reader[10].SQLiteType != SQLiteType.Null) - { - info.DeviceName = reader[10].ToString(); - } - - return info; - } - - public DeviceOptions GetDeviceOptions(string deviceId) - { - using (var connection = GetConnection(true)) - { - return connection.RunInTransaction( - db => - { - using (var statement = base.PrepareStatement(db, "select CustomName from Devices where Id=@DeviceId")) - { - statement.TryBind("@DeviceId", deviceId); - - var result = new DeviceOptions(deviceId); - - foreach (var row in statement.ExecuteQuery()) - { - if (row[0].SQLiteType != SQLiteType.Null) - { - result.CustomName = row[0].ToString(); - } - } - - return result; - } - }, ReadTransactionMode); - } - } - - public void UpdateDeviceOptions(string deviceId, DeviceOptions options) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => - { - using (var statement = db.PrepareStatement("replace into devices (Id, CustomName, Capabilities) VALUES (@Id, @CustomName, (Select Capabilities from Devices where Id=@Id))")) - { - statement.TryBind("@Id", deviceId); - - if (string.IsNullOrWhiteSpace(options.CustomName)) - { - statement.TryBindNull("@CustomName"); - } - else - { - statement.TryBind("@CustomName", options.CustomName); - } - - statement.MoveNext(); - } - }, TransactionMode); - } - } - } -} diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 2fb5040f9c..92ef65bf11 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -11,6 +11,7 @@ using Jellyfin.Data.Entities; using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; +using Jellyfin.Data.Queries; using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; @@ -23,7 +24,6 @@ using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Events.Session; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -52,7 +52,6 @@ namespace Emby.Server.Implementations.Session private readonly IImageProcessor _imageProcessor; private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerApplicationHost _appHost; - private readonly IAuthenticationRepository _authRepo; private readonly IDeviceManager _deviceManager; /// @@ -75,7 +74,6 @@ namespace Emby.Server.Implementations.Session IDtoService dtoService, IImageProcessor imageProcessor, IServerApplicationHost appHost, - IAuthenticationRepository authRepo, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager) { @@ -88,7 +86,6 @@ namespace Emby.Server.Implementations.Session _dtoService = dtoService; _imageProcessor = imageProcessor; _appHost = appHost; - _authRepo = authRepo; _deviceManager = deviceManager; _mediaSourceManager = mediaSourceManager; @@ -1486,7 +1483,7 @@ namespace Emby.Server.Implementations.Session throw new SecurityException("User is at their maximum number of sessions."); } - var token = GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName); + var token = await GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName).ConfigureAwait(false); var session = await LogSessionActivity( request.App, @@ -1509,21 +1506,21 @@ namespace Emby.Server.Implementations.Session return returnResult; } - private string GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName) + private async Task GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName) { - var existing = _authRepo.Get( - new AuthenticationInfoQuery + var existing = (await _deviceManager.GetDevices( + new DeviceQuery { DeviceId = deviceId, UserId = user.Id, Limit = 1 - }).Items.FirstOrDefault(); + }).ConfigureAwait(false)).Items.FirstOrDefault(); - var allExistingForDevice = _authRepo.Get( - new AuthenticationInfoQuery + var allExistingForDevice = (await _deviceManager.GetDevices( + new DeviceQuery { DeviceId = deviceId - }).Items; + }).ConfigureAwait(false)).Items; foreach (var auth in allExistingForDevice) { @@ -1531,7 +1528,7 @@ namespace Emby.Server.Implementations.Session { try { - Logout(auth); + await Logout(auth).ConfigureAwait(false); } catch (Exception ex) { @@ -1546,29 +1543,14 @@ namespace Emby.Server.Implementations.Session return existing.AccessToken; } - var now = DateTime.UtcNow; - - var newToken = new AuthenticationInfo - { - AppName = app, - AppVersion = appVersion, - DateCreated = now, - DateLastActivity = now, - DeviceId = deviceId, - DeviceName = deviceName, - UserId = user.Id, - AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), - UserName = user.Username - }; - _logger.LogInformation("Creating new access token for user {0}", user.Id); - _authRepo.Create(newToken); + var device = await _deviceManager.CreateDevice(new Device(user.Id, app, appVersion, deviceName, deviceId)).ConfigureAwait(false); - return newToken.AccessToken; + return device.AccessToken; } /// - public void Logout(string accessToken) + public async Task Logout(string accessToken) { CheckDisposed(); @@ -1577,27 +1559,27 @@ namespace Emby.Server.Implementations.Session throw new ArgumentNullException(nameof(accessToken)); } - var existing = _authRepo.Get( - new AuthenticationInfoQuery + var existing = (await _deviceManager.GetDevices( + new DeviceQuery { Limit = 1, AccessToken = accessToken - }).Items; + }).ConfigureAwait(false)).Items; if (existing.Count > 0) { - Logout(existing[0]); + await Logout(existing[0]).ConfigureAwait(false); } } /// - public void Logout(AuthenticationInfo existing) + public async Task Logout(Device existing) { CheckDisposed(); _logger.LogInformation("Logging out access token {0}", existing.AccessToken); - _authRepo.Delete(existing); + await _deviceManager.DeleteDevice(existing).ConfigureAwait(false); var sessions = Sessions .Where(i => string.Equals(i.DeviceId, existing.DeviceId, StringComparison.OrdinalIgnoreCase)) @@ -1617,30 +1599,24 @@ namespace Emby.Server.Implementations.Session } /// - public void RevokeUserTokens(Guid userId, string currentAccessToken) + public async Task RevokeUserTokens(Guid userId, string currentAccessToken) { CheckDisposed(); - var existing = _authRepo.Get(new AuthenticationInfoQuery + var existing = await _deviceManager.GetDevices(new DeviceQuery { UserId = userId - }); + }).ConfigureAwait(false); foreach (var info in existing.Items) { if (!string.Equals(currentAccessToken, info.AccessToken, StringComparison.OrdinalIgnoreCase)) { - Logout(info); + await Logout(info).ConfigureAwait(false); } } } - /// - public void RevokeToken(string token) - { - Logout(token); - } - /// /// Reports the capabilities. /// @@ -1792,7 +1768,7 @@ namespace Emby.Server.Implementations.Session } /// - public Task GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion) + public Task GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, string appVersion) { if (info == null) { @@ -1825,20 +1801,20 @@ namespace Emby.Server.Implementations.Session } /// - public Task GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint) + public async Task GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint) { - var items = _authRepo.Get(new AuthenticationInfoQuery + var items = (await _deviceManager.GetDevices(new DeviceQuery { AccessToken = token, Limit = 1 - }).Items; + }).ConfigureAwait(false)).Items; if (items.Count == 0) { return null; } - return GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null); + return await GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null).ConfigureAwait(false); } /// diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs index c56233794a..369e846aef 100644 --- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs +++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs @@ -40,11 +40,11 @@ namespace Jellyfin.Api.Auth } /// - protected override Task HandleAuthenticateAsync() + protected override async Task HandleAuthenticateAsync() { try { - var authorizationInfo = _authService.Authenticate(Request); + var authorizationInfo = await _authService.Authenticate(Request).ConfigureAwait(false); var role = UserRoles.User; if (authorizationInfo.IsApiKey || authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator)) { @@ -68,16 +68,16 @@ namespace Jellyfin.Api.Auth var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, Scheme.Name); - return Task.FromResult(AuthenticateResult.Success(ticket)); + return AuthenticateResult.Success(ticket); } catch (AuthenticationException ex) { _logger.LogDebug(ex, "Error authenticating with {Handler}", nameof(CustomAuthenticationHandler)); - return Task.FromResult(AuthenticateResult.NoResult()); + return AuthenticateResult.NoResult(); } catch (SecurityException ex) { - return Task.FromResult(AuthenticateResult.Fail(ex)); + return AuthenticateResult.Fail(ex); } } } diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs index 852d1e9cbd..8a98d856c4 100644 --- a/Jellyfin.Api/Controllers/CollectionController.cs +++ b/Jellyfin.Api/Controllers/CollectionController.cs @@ -58,7 +58,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] Guid? parentId, [FromQuery] bool isLocked = false) { - var userId = _authContext.GetAuthorizationInfo(Request).UserId; + var userId = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).UserId; var item = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions { diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 4cfae568ad..8af7b8f734 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -3,8 +3,8 @@ using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Data.Entities.Security; +using Jellyfin.Data.Queries; using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Devices; using MediaBrowser.Model.Querying; @@ -21,22 +21,18 @@ namespace Jellyfin.Api.Controllers public class DevicesController : BaseJellyfinApiController { private readonly IDeviceManager _deviceManager; - private readonly IAuthenticationRepository _authenticationRepository; private readonly ISessionManager _sessionManager; /// /// Initializes a new instance of the class. /// /// Instance of interface. - /// Instance of interface. /// Instance of interface. public DevicesController( IDeviceManager deviceManager, - IAuthenticationRepository authenticationRepository, ISessionManager sessionManager) { _deviceManager = deviceManager; - _authenticationRepository = authenticationRepository; _sessionManager = sessionManager; } @@ -111,7 +107,7 @@ namespace Jellyfin.Api.Controllers [FromQuery, Required] string id, [FromBody, Required] DeviceOptions deviceOptions) { - var existingDeviceOptions = _deviceManager.GetDeviceOptions(id); + var existingDeviceOptions = await _deviceManager.GetDeviceOptions(id).ConfigureAwait(false); if (existingDeviceOptions == null) { return NotFound(); @@ -131,19 +127,19 @@ namespace Jellyfin.Api.Controllers [HttpDelete] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult DeleteDevice([FromQuery, Required] string id) + public async Task DeleteDevice([FromQuery, Required] string id) { - var existingDevice = _deviceManager.GetDevice(id); + var existingDevice = await _deviceManager.GetDevice(id).ConfigureAwait(false); if (existingDevice == null) { return NotFound(); } - var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items; + var sessions = await _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }).ConfigureAwait(false); - foreach (var session in sessions) + foreach (var session in sessions.Items) { - _sessionManager.Logout(session); + await _sessionManager.Logout(session).ConfigureAwait(false); } return NoContent(); diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 8f7500ac69..9dc280e138 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -97,7 +97,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] ImageType imageType, [FromQuery] int? index = null) { - if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) + if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image."); } @@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] ImageType imageType, [FromRoute] int index) { - if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) + if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image."); } @@ -190,7 +190,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] ImageType imageType, [FromQuery] int? index = null) { - if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) + if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image."); } @@ -234,7 +234,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] ImageType imageType, [FromRoute] int index) { - if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) + if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image."); } diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 4ed15e1d51..504f587906 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -331,10 +331,10 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public ActionResult DeleteItem(Guid itemId) + public async Task DeleteItem(Guid itemId) { var item = _libraryManager.GetItemById(itemId); - var auth = _authContext.GetAuthorizationInfo(Request); + var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); var user = auth.User; if (!item.CanDelete(user)) @@ -361,7 +361,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) + public async Task DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) { if (ids.Length == 0) { @@ -371,7 +371,7 @@ namespace Jellyfin.Api.Controllers foreach (var i in ids) { var item = _libraryManager.GetItemById(i); - var auth = _authContext.GetAuthorizationInfo(Request); + var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); var user = auth.User; if (!item.CanDelete(user)) @@ -627,7 +627,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - var auth = _authContext.GetAuthorizationInfo(Request); + var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); var user = auth.User; diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index e330f02b61..6c29e6bb84 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -123,7 +123,7 @@ namespace Jellyfin.Api.Controllers [FromQuery, ParameterObsolete] bool? allowAudioStreamCopy, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto) { - var authInfo = _authContext.GetAuthorizationInfo(Request); + var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); var profile = playbackInfoDto?.DeviceProfile; _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile); diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs index cc8c630b35..6dee1c2192 100644 --- a/Jellyfin.Api/Controllers/PlaystateController.cs +++ b/Jellyfin.Api/Controllers/PlaystateController.cs @@ -171,7 +171,8 @@ namespace Jellyfin.Api.Controllers _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty); if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId)) { - await _transcodingJobHelper.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); + var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); + await _transcodingJobHelper.KillTranscodingJobs(authInfo.DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); } playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); @@ -320,7 +321,8 @@ namespace Jellyfin.Api.Controllers _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty); if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId)) { - await _transcodingJobHelper.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); + var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); + await _transcodingJobHelper.KillTranscodingJobs(authInfo.DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); } playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 116d669e40..3a04cb3a4a 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -471,11 +471,11 @@ namespace Jellyfin.Api.Controllers [HttpPost("Sessions/Logout")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult ReportSessionEnded() + public async Task ReportSessionEnded() { - AuthorizationInfo auth = _authContext.GetAuthorizationInfo(Request); + AuthorizationInfo auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); - _sessionManager.Logout(auth.Token); + await _sessionManager.Logout(auth.Token).ConfigureAwait(false); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 1669a659dc..2d5339b166 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -361,7 +361,7 @@ namespace Jellyfin.Api.Controllers long positionTicks = 0; - var accessToken = _authContext.GetAuthorizationInfo(Request).Token; + var accessToken = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Token; while (positionTicks < runtime) { diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 679f055bc2..20a02bf4a9 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -116,9 +116,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool enableRedirection = true) { var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels); - _authorizationContext.GetAuthorizationInfo(Request).DeviceId = deviceId; + (await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).DeviceId = deviceId; - var authInfo = _authorizationContext.GetAuthorizationInfo(Request); + var authInfo = await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false); _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile); diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index b13db4baa9..8e2298bb7d 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -77,11 +77,11 @@ namespace Jellyfin.Api.Controllers [HttpGet] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetUsers( + public async Task>> GetUsers( [FromQuery] bool? isHidden, [FromQuery] bool? isDisabled) { - var users = Get(isHidden, isDisabled, false, false); + var users = await Get(isHidden, isDisabled, false, false).ConfigureAwait(false); return Ok(users); } @@ -92,15 +92,15 @@ namespace Jellyfin.Api.Controllers /// An containing the public users. [HttpGet("Public")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetPublicUsers() + public async Task>> GetPublicUsers() { // If the startup wizard hasn't been completed then just return all users if (!_config.Configuration.IsStartupWizardCompleted) { - return Ok(Get(false, false, false, false)); + return Ok(await Get(false, false, false, false).ConfigureAwait(false)); } - return Ok(Get(false, false, true, true)); + return Ok(await Get(false, false, true, true).ConfigureAwait(false)); } /// @@ -141,7 +141,7 @@ namespace Jellyfin.Api.Controllers public async Task DeleteUser([FromRoute, Required] Guid userId) { var user = _userManager.GetUserById(userId); - _sessionManager.RevokeUserTokens(user.Id, null); + await _sessionManager.RevokeUserTokens(user.Id, null).ConfigureAwait(false); await _userManager.DeleteUserAsync(userId).ConfigureAwait(false); return NoContent(); } @@ -195,7 +195,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public async Task> AuthenticateUserByName([FromBody, Required] AuthenticateUserByName request) { - var auth = _authContext.GetAuthorizationInfo(Request); + var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); try { @@ -230,7 +230,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public async Task> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request) { - var auth = _authContext.GetAuthorizationInfo(Request); + var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); try { @@ -271,7 +271,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid userId, [FromBody, Required] UpdateUserPassword request) { - if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) + if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the password."); } @@ -303,9 +303,9 @@ namespace Jellyfin.Api.Controllers await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false); - var currentToken = _authContext.GetAuthorizationInfo(Request).Token; + var currentToken = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Token; - _sessionManager.RevokeUserTokens(user.Id, currentToken); + await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false); } return NoContent(); @@ -325,11 +325,11 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UpdateUserEasyPassword( + public async Task UpdateUserEasyPassword( [FromRoute, Required] Guid userId, [FromBody, Required] UpdateUserEasyPassword request) { - if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) + if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the easy password."); } @@ -343,11 +343,11 @@ namespace Jellyfin.Api.Controllers if (request.ResetPassword) { - _userManager.ResetEasyPassword(user); + await _userManager.ResetEasyPassword(user).ConfigureAwait(false); } else { - _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword); + await _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword).ConfigureAwait(false); } return NoContent(); @@ -371,7 +371,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid userId, [FromBody, Required] UserDto updateUser) { - if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false)) + if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false).ConfigureAwait(false)) { return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed."); } @@ -431,8 +431,8 @@ namespace Jellyfin.Api.Controllers return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one enabled user in the system."); } - var currentToken = _authContext.GetAuthorizationInfo(Request).Token; - _sessionManager.RevokeUserTokens(user.Id, currentToken); + var currentToken = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Token; + await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false); } await _userManager.UpdatePolicyAsync(userId, newPolicy).ConfigureAwait(false); @@ -456,7 +456,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid userId, [FromBody, Required] UserConfiguration userConfig) { - if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false)) + if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false).ConfigureAwait(false)) { return StatusCode(StatusCodes.Status403Forbidden, "User configuration update not allowed"); } @@ -555,7 +555,7 @@ namespace Jellyfin.Api.Controllers return _userManager.GetUserDto(user); } - private IEnumerable Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork) + private async Task> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork) { var users = _userManager.Users; @@ -571,7 +571,7 @@ namespace Jellyfin.Api.Controllers if (filterByDevice) { - var deviceId = _authContext.GetAuthorizationInfo(Request).DeviceId; + var deviceId = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).DeviceId; if (!string.IsNullOrWhiteSpace(deviceId)) { diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index 7bc5ecdf1d..3d27371f63 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; +using System.Threading.Tasks; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.UserViewDtos; @@ -64,7 +65,7 @@ namespace Jellyfin.Api.Controllers /// An containing the user views. [HttpGet("Users/{userId}/Views")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetUserViews( + public async Task>> GetUserViews( [FromRoute, Required] Guid userId, [FromQuery] bool? includeExternalContent, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews, @@ -86,7 +87,7 @@ namespace Jellyfin.Api.Controllers query.PresetViews = presetViews; } - var app = _authContext.GetAuthorizationInfo(Request).Client ?? string.Empty; + var app = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Client ?? string.Empty; if (app.IndexOf("emby rt", StringComparison.OrdinalIgnoreCase) != -1) { query.PresetViews = new[] { CollectionType.Movies, CollectionType.TvShows }; diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 295cfaf089..3b8dc7e316 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -468,7 +468,7 @@ namespace Jellyfin.Api.Helpers /// A containing the . public async Task OpenMediaSource(HttpRequest httpRequest, LiveStreamRequest request) { - var authInfo = _authContext.GetAuthorizationInfo(httpRequest); + var authInfo = await _authContext.GetAuthorizationInfo(httpRequest).ConfigureAwait(false); var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false); diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 3810f74779..0efd3443b8 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -60,9 +60,9 @@ namespace Jellyfin.Api.Helpers /// The user id. /// Whether to restrict the user preferences. /// A whether the user can update the entry. - internal static bool AssertCanUpdateUser(IAuthorizationContext authContext, HttpRequest requestContext, Guid userId, bool restrictUserPreferences) + internal static async Task AssertCanUpdateUser(IAuthorizationContext authContext, HttpRequest requestContext, Guid userId, bool restrictUserPreferences) { - var auth = authContext.GetAuthorizationInfo(requestContext); + var auth = await authContext.GetAuthorizationInfo(requestContext).ConfigureAwait(false); var authenticatedUser = auth.User; @@ -78,7 +78,7 @@ namespace Jellyfin.Api.Helpers internal static async Task GetSession(ISessionManager sessionManager, IAuthorizationContext authContext, HttpRequest request) { - var authorization = authContext.GetAuthorizationInfo(request); + var authorization = await authContext.GetAuthorizationInfo(request).ConfigureAwait(false); var user = authorization.User; var session = await sessionManager.LogSessionActivity( authorization.Client, diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 8cffe9c4c9..cecbd36c12 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -17,9 +17,7 @@ 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; @@ -101,7 +99,7 @@ namespace Jellyfin.Api.Helpers EnableDlnaHeaders = enableDlnaHeaders }; - var auth = authorizationContext.GetAuthorizationInfo(httpRequest); + var auth = await authorizationContext.GetAuthorizationInfo(httpRequest).ConfigureAwait(false); if (!auth.UserId.Equals(Guid.Empty)) { state.User = userManager.GetUserById(auth.UserId); diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 0879cbd18f..2420779633 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -489,7 +489,7 @@ namespace Jellyfin.Api.Helpers if (state.VideoRequest != null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) { - var auth = _authorizationContext.GetAuthorizationInfo(request); + var auth = await _authorizationContext.GetAuthorizationInfo(request).ConfigureAwait(false); if (auth.User != null && !auth.User.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)) { this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state); diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index bde8eb86ee..9fd2e5ad4c 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -6,6 +6,7 @@ using Jellyfin.Data.Entities; using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; +using Jellyfin.Data.Queries; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Devices; @@ -55,6 +56,17 @@ namespace Jellyfin.Server.Implementations.Devices DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs>(new Tuple(deviceId, options))); } + /// + public async Task CreateDevice(Device device) + { + await using var dbContext = _dbProvider.CreateContext(); + + dbContext.Devices.Add(device); + + await dbContext.SaveChangesAsync().ConfigureAwait(false); + return device; + } + /// public async Task GetDeviceOptions(string deviceId) { @@ -90,6 +102,61 @@ namespace Jellyfin.Server.Implementations.Devices return deviceInfo; } + /// + public async Task> GetDevices(DeviceQuery query) + { + await using var dbContext = _dbProvider.CreateContext(); + + var devices = dbContext.Devices.AsQueryable(); + + if (query.UserId.HasValue) + { + devices = devices.Where(device => device.UserId == query.UserId.Value); + } + + if (query.DeviceId != null) + { + devices = devices.Where(device => device.DeviceId == query.DeviceId); + } + + if (query.AccessToken != null) + { + devices = devices.Where(device => device.AccessToken == query.AccessToken); + } + + if (query.Skip.HasValue) + { + devices = devices.Skip(query.Skip.Value); + } + + var count = await devices.CountAsync().ConfigureAwait(false); + + if (query.Limit.HasValue) + { + devices = devices.Take(query.Limit.Value); + } + + return new QueryResult + { + Items = await devices.ToListAsync().ConfigureAwait(false), + StartIndex = query.Skip ?? 0, + TotalRecordCount = count + }; + } + + /// + public async Task> GetDeviceInfos(DeviceQuery query) + { + var devices = await GetDevices(query).ConfigureAwait(false); + + return new QueryResult + { + Items = devices.Items.Select(ToDeviceInfo).ToList(), + StartIndex = devices.StartIndex, + TotalRecordCount = devices.TotalRecordCount + }; + } + /// public async Task> GetDevicesForUser(Guid? userId, bool? supportsSync) { @@ -117,6 +184,12 @@ namespace Jellyfin.Server.Implementations.Devices return new QueryResult(array); } + /// + public async Task DeleteDevice(Device device) + { + await using var dbContext = _dbProvider.CreateContext(); + } + /// public bool CanAccessDevice(User user, string deviceId) { diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index 059e884e5f..6f35a2c1c2 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -214,6 +214,15 @@ namespace Jellyfin.Server.Implementations modelBuilder.Entity() .HasIndex(entity => new { entity.DeviceId, entity.DateLastActivity }); + modelBuilder.Entity() + .HasIndex(entity => new { entity.AccessToken, entity.DateLastActivity }); + + modelBuilder.Entity() + .HasIndex(entity => new { entity.UserId, entity.DeviceId }); + + modelBuilder.Entity() + .HasIndex(entity => entity.DeviceId); + modelBuilder.Entity() .HasIndex(entity => entity.DeviceId) .IsUnique(); diff --git a/Jellyfin.Server.Implementations/Migrations/20210521032224_AddDevices.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20210521032224_AddDevices.Designer.cs new file mode 100644 index 0000000000..e1faef7a2f --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20210521032224_AddDevices.Designer.cs @@ -0,0 +1,645 @@ +#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("20210521032224_AddDevices")] + partial class AddDevices + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("jellyfin") + .HasAnnotation("ProductVersion", "5.0.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") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + 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() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .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("ItemDisplayPreferences"); + }); + + 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("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + 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("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EasyPassword") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + 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("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + 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") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + 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") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20210521032224_AddDevices.cs b/Jellyfin.Server.Implementations/Migrations/20210521032224_AddDevices.cs new file mode 100644 index 0000000000..2da8d37881 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20210521032224_AddDevices.cs @@ -0,0 +1,126 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1601 +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Jellyfin.Server.Implementations.Migrations +{ + public partial class AddDevices : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ApiKeys", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DateCreated = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 64, nullable: false), + AccessToken = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiKeys", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "DeviceOptions", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DeviceId = table.Column(type: "TEXT", nullable: false), + CustomName = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_DeviceOptions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Devices", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(type: "TEXT", nullable: false), + AccessToken = table.Column(type: "TEXT", nullable: false), + AppName = table.Column(type: "TEXT", maxLength: 64, nullable: false), + AppVersion = table.Column(type: "TEXT", maxLength: 32, nullable: false), + DeviceName = table.Column(type: "TEXT", maxLength: 64, nullable: false), + DeviceId = table.Column(type: "TEXT", maxLength: 256, nullable: false), + IsActive = table.Column(type: "INTEGER", nullable: false), + DateCreated = table.Column(type: "TEXT", nullable: false), + DateLastActivity = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Devices", x => x.Id); + table.ForeignKey( + name: "FK_Devices_Users_UserId", + column: x => x.UserId, + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeys_AccessToken", + schema: "jellyfin", + table: "ApiKeys", + column: "AccessToken", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_DeviceOptions_DeviceId", + schema: "jellyfin", + table: "DeviceOptions", + column: "DeviceId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Devices_AccessToken_DateLastActivity", + schema: "jellyfin", + table: "Devices", + columns: new[] { "AccessToken", "DateLastActivity" }); + + migrationBuilder.CreateIndex( + name: "IX_Devices_DeviceId", + schema: "jellyfin", + table: "Devices", + column: "DeviceId"); + + migrationBuilder.CreateIndex( + name: "IX_Devices_DeviceId_DateLastActivity", + schema: "jellyfin", + table: "Devices", + columns: new[] { "DeviceId", "DateLastActivity" }); + + migrationBuilder.CreateIndex( + name: "IX_Devices_UserId_DeviceId", + schema: "jellyfin", + table: "Devices", + columns: new[] { "UserId", "DeviceId" }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ApiKeys", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "DeviceOptions", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "Devices", + schema: "jellyfin"); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index 286eb7468a..8a1ae16f84 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", "5.0.3"); + .HasAnnotation("ProductVersion", "5.0.5"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -332,6 +332,107 @@ namespace Jellyfin.Server.Implementations.Migrations b.ToTable("Preferences"); }); + modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => { b.Property("Id") @@ -505,6 +606,17 @@ namespace Jellyfin.Server.Implementations.Migrations .OnDelete(DeleteBehavior.Cascade); }); + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => { b.Navigation("HomeSections"); diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs similarity index 64% rename from Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs rename to Jellyfin.Server.Implementations/Security/AuthorizationContext.cs index 024404ceb0..775edafc2d 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs @@ -4,39 +4,39 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net; +using System.Threading.Tasks; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Security; using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; -namespace Emby.Server.Implementations.HttpServer.Security +namespace Jellyfin.Server.Implementations.Security { public class AuthorizationContext : IAuthorizationContext { - private readonly IAuthenticationRepository _authRepo; + private readonly JellyfinDb _jellyfinDb; private readonly IUserManager _userManager; - public AuthorizationContext(IAuthenticationRepository authRepo, IUserManager userManager) + public AuthorizationContext(JellyfinDb jellyfinDb, IUserManager userManager) { - _authRepo = authRepo; + _jellyfinDb = jellyfinDb; _userManager = userManager; } - public AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext) + public Task GetAuthorizationInfo(HttpContext requestContext) { - if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached)) + if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached) && cached != null) { - return (AuthorizationInfo)cached; + return Task.FromResult((AuthorizationInfo)cached); } return GetAuthorization(requestContext); } - public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext) + public async Task GetAuthorizationInfo(HttpRequest requestContext) { var auth = GetAuthorizationDictionary(requestContext); - var authInfo = GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query); + var authInfo = await GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query).ConfigureAwait(false); return authInfo; } @@ -45,35 +45,37 @@ namespace Emby.Server.Implementations.HttpServer.Security /// /// The HTTP req. /// Dictionary{System.StringSystem.String}. - private AuthorizationInfo GetAuthorization(HttpContext httpReq) + private async Task GetAuthorization(HttpContext httpReq) { var auth = GetAuthorizationDictionary(httpReq); - var authInfo = GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query); + var authInfo = await GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query).ConfigureAwait(false); httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo; return authInfo; } - private AuthorizationInfo GetAuthorizationInfoFromDictionary( - in Dictionary auth, - in IHeaderDictionary headers, - in IQueryCollection queryString) + private async Task GetAuthorizationInfoFromDictionary( + IReadOnlyDictionary? auth, + IHeaderDictionary headers, + IQueryCollection queryString) { - string deviceId = null; - string device = null; - string client = null; - string version = null; - string token = null; + string? deviceId = null; + string? deviceName = null; + string? client = null; + string? version = null; + string? token = null; if (auth != null) { auth.TryGetValue("DeviceId", out deviceId); - auth.TryGetValue("Device", out device); + auth.TryGetValue("Device", out deviceName); auth.TryGetValue("Client", out client); auth.TryGetValue("Version", out version); auth.TryGetValue("Token", out token); } +#pragma warning disable CA1508 + // headers can return StringValues.Empty if (string.IsNullOrEmpty(token)) { token = headers["X-Emby-Token"]; @@ -98,7 +100,7 @@ namespace Emby.Server.Implementations.HttpServer.Security var authInfo = new AuthorizationInfo { Client = client, - Device = device, + Device = deviceName, DeviceId = deviceId, Version = version, Token = token, @@ -111,80 +113,69 @@ namespace Emby.Server.Implementations.HttpServer.Security // Request doesn't contain a token. return authInfo; } +#pragma warning restore CA1508 authInfo.HasToken = true; - var result = _authRepo.Get(new AuthenticationInfoQuery - { - AccessToken = token - }); + var device = await _jellyfinDb.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false); - if (result.Items.Count > 0) + if (device != null) { authInfo.IsAuthenticated = true; } - var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null; - - if (originalAuthenticationInfo != null) + if (device != null) { var updateToken = false; // TODO: Remove these checks for IsNullOrWhiteSpace if (string.IsNullOrWhiteSpace(authInfo.Client)) { - authInfo.Client = originalAuthenticationInfo.AppName; + authInfo.Client = device.AppName; } if (string.IsNullOrWhiteSpace(authInfo.DeviceId)) { - authInfo.DeviceId = originalAuthenticationInfo.DeviceId; + authInfo.DeviceId = device.DeviceId; } // Temporary. TODO - allow clients to specify that the token has been shared with a casting device - var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1; + var allowTokenInfoUpdate = !authInfo.Client.Contains("chromecast", StringComparison.OrdinalIgnoreCase); if (string.IsNullOrWhiteSpace(authInfo.Device)) { - authInfo.Device = originalAuthenticationInfo.DeviceName; + authInfo.Device = device.DeviceName; } - else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase)) + else if (!string.Equals(authInfo.Device, device.DeviceName, StringComparison.OrdinalIgnoreCase)) { if (allowTokenInfoUpdate) { updateToken = true; - originalAuthenticationInfo.DeviceName = authInfo.Device; + device.DeviceName = authInfo.Device; } } if (string.IsNullOrWhiteSpace(authInfo.Version)) { - authInfo.Version = originalAuthenticationInfo.AppVersion; + authInfo.Version = device.AppVersion; } - else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase)) + else if (!string.Equals(authInfo.Version, device.AppVersion, StringComparison.OrdinalIgnoreCase)) { if (allowTokenInfoUpdate) { updateToken = true; - originalAuthenticationInfo.AppVersion = authInfo.Version; + device.AppVersion = authInfo.Version; } } - if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3) + if ((DateTime.UtcNow - device.DateLastActivity).TotalMinutes > 3) { - originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow; + device.DateLastActivity = DateTime.UtcNow; updateToken = true; } - if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty)) + if (!device.UserId.Equals(Guid.Empty)) { - authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId); - - if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase)) - { - originalAuthenticationInfo.UserName = authInfo.User.Username; - updateToken = true; - } - + authInfo.User = _userManager.GetUserById(device.UserId); authInfo.IsApiKey = false; } else @@ -194,7 +185,8 @@ namespace Emby.Server.Implementations.HttpServer.Security if (updateToken) { - _authRepo.Update(originalAuthenticationInfo); + _jellyfinDb.Devices.Update(device); + await _jellyfinDb.SaveChangesAsync().ConfigureAwait(false); } } @@ -206,7 +198,7 @@ namespace Emby.Server.Implementations.HttpServer.Security /// /// The HTTP req. /// Dictionary{System.StringSystem.String}. - private Dictionary GetAuthorizationDictionary(HttpContext httpReq) + private Dictionary? GetAuthorizationDictionary(HttpContext httpReq) { var auth = httpReq.Request.Headers["X-Emby-Authorization"]; @@ -223,7 +215,7 @@ namespace Emby.Server.Implementations.HttpServer.Security /// /// The HTTP req. /// Dictionary{System.StringSystem.String}. - private Dictionary GetAuthorizationDictionary(HttpRequest httpReq) + private Dictionary? GetAuthorizationDictionary(HttpRequest httpReq) { var auth = httpReq.Headers["X-Emby-Authorization"]; @@ -240,7 +232,7 @@ namespace Emby.Server.Implementations.HttpServer.Security /// /// The authorization header. /// Dictionary{System.StringSystem.String}. - private Dictionary GetAuthorization(string authorizationHeader) + private Dictionary? GetAuthorization(string? authorizationHeader) { if (authorizationHeader == null) { diff --git a/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs b/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs index dbba80c210..a471ea1d50 100644 --- a/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs +++ b/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs @@ -4,10 +4,10 @@ using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; +using Jellyfin.Data.Queries; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; -using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; namespace Jellyfin.Server.Implementations.Users @@ -15,14 +15,12 @@ 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) + public DeviceAccessEntryPoint(IUserManager userManager, IDeviceManager deviceManager, ISessionManager sessionManager) { _userManager = userManager; - _authRepo = authRepo; _deviceManager = deviceManager; _sessionManager = sessionManager; } @@ -38,27 +36,27 @@ namespace Jellyfin.Server.Implementations.Users { } - private void OnUserUpdated(object? sender, GenericEventArgs e) + private async void OnUserUpdated(object? sender, GenericEventArgs e) { var user = e.Argument; if (!user.HasPermission(PermissionKind.EnableAllDevices)) { - UpdateDeviceAccess(user); + await UpdateDeviceAccess(user).ConfigureAwait(false); } } - private void UpdateDeviceAccess(User user) + private async Task UpdateDeviceAccess(User user) { - var existing = _authRepo.Get(new AuthenticationInfoQuery + var existing = (await _deviceManager.GetDevices(new DeviceQuery { UserId = user.Id - }).Items; + }).ConfigureAwait(false)).Items; - foreach (var authInfo in existing) + foreach (var device in existing) { - if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId)) + if (!string.IsNullOrEmpty(device.DeviceId) && !_deviceManager.CanAccessDevice(user, device.DeviceId)) { - _sessionManager.Logout(authInfo); + await _sessionManager.Logout(device).ConfigureAwait(false); } } } diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index b20acae32e..362be85316 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -11,6 +11,7 @@ using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Activity; using Jellyfin.Server.Implementations.Devices; using Jellyfin.Server.Implementations.Events; +using Jellyfin.Server.Implementations.Security; using Jellyfin.Server.Implementations.Users; using MediaBrowser.Controller; using MediaBrowser.Controller.BaseItemManager; @@ -94,6 +95,8 @@ namespace Jellyfin.Server ServiceCollection.AddSingleton(); ServiceCollection.AddSingleton(); + ServiceCollection.AddScoped(); + base.RegisterServices(); } diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index 28612cea33..6ff4422d21 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Events; +using Jellyfin.Data.Queries; using MediaBrowser.Model.Devices; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; @@ -17,6 +18,13 @@ namespace MediaBrowser.Controller.Devices { event EventHandler>> DeviceOptionsUpdated; + /// + /// Creates a new device. + /// + /// The device to create. + /// A representing the creation of the device. + Task CreateDevice(Device device); + /// /// Saves the capabilities. /// @@ -38,6 +46,15 @@ namespace MediaBrowser.Controller.Devices /// DeviceInfo. Task GetDevice(string id); + /// + /// Gets devices based on the provided query. + /// + /// The device query. + /// A representing the retrieval of the devices. + Task> GetDevices(DeviceQuery query); + + Task> GetDeviceInfos(DeviceQuery query); + /// /// Gets the devices. /// @@ -46,6 +63,8 @@ namespace MediaBrowser.Controller.Devices /// IEnumerable<DeviceInfo>. Task> GetDevicesForUser(Guid? userId, bool? supportsSync); + Task DeleteDevice(Device device); + /// /// Determines whether this instance [can access device] the specified user identifier. /// diff --git a/MediaBrowser.Controller/Net/IAuthService.cs b/MediaBrowser.Controller/Net/IAuthService.cs index d15c6d3183..a7da740e05 100644 --- a/MediaBrowser.Controller/Net/IAuthService.cs +++ b/MediaBrowser.Controller/Net/IAuthService.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net @@ -12,6 +13,6 @@ namespace MediaBrowser.Controller.Net /// /// The request. /// Authorization information. Null if unauthenticated. - AuthorizationInfo Authenticate(HttpRequest request); + Task Authenticate(HttpRequest request); } } diff --git a/MediaBrowser.Controller/Net/IAuthorizationContext.cs b/MediaBrowser.Controller/Net/IAuthorizationContext.cs index 0d310548dc..5c6ca43d1e 100644 --- a/MediaBrowser.Controller/Net/IAuthorizationContext.cs +++ b/MediaBrowser.Controller/Net/IAuthorizationContext.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net @@ -11,14 +12,14 @@ namespace MediaBrowser.Controller.Net /// Gets the authorization information. /// /// The request context. - /// AuthorizationInfo. - AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext); + /// A task containing the authorization info. + Task GetAuthorizationInfo(HttpContext requestContext); /// /// Gets the authorization information. /// /// The request context. - /// AuthorizationInfo. - AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext); + /// A containing the authorization info. + Task GetAuthorizationInfo(HttpRequest requestContext); } } diff --git a/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs b/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs deleted file mode 100644 index 3af6a525c7..0000000000 --- a/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs +++ /dev/null @@ -1,53 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; - -namespace MediaBrowser.Controller.Security -{ - public class AuthenticationInfoQuery - { - /// - /// Gets or sets the device identifier. - /// - /// The device identifier. - public string DeviceId { get; set; } - - /// - /// Gets or sets the user identifier. - /// - /// The user identifier. - public Guid UserId { get; set; } - - /// - /// Gets or sets the access token. - /// - /// The access token. - public string AccessToken { get; set; } - - /// - /// Gets or sets a value indicating whether this instance is active. - /// - /// null if [is active] contains no value, true if [is active]; otherwise, false. - public bool? IsActive { get; set; } - - /// - /// Gets or sets a value indicating whether this instance has user. - /// - /// null if [has user] contains no value, true if [has user]; otherwise, false. - public bool? HasUser { get; set; } - - /// - /// Gets or sets the start index. - /// - /// The start index. - public int? StartIndex { get; set; } - - /// - /// Gets or sets the limit. - /// - /// The limit. - public int? Limit { get; set; } - } -} diff --git a/MediaBrowser.Controller/Security/IAuthenticationRepository.cs b/MediaBrowser.Controller/Security/IAuthenticationRepository.cs deleted file mode 100644 index 9685005ba3..0000000000 --- a/MediaBrowser.Controller/Security/IAuthenticationRepository.cs +++ /dev/null @@ -1,39 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using Jellyfin.Data.Entities.Security; -using MediaBrowser.Model.Querying; - -namespace MediaBrowser.Controller.Security -{ - public interface IAuthenticationRepository - { - /// - /// Creates the specified information. - /// - /// The information. - /// Task. - void Create(AuthenticationInfo info); - - /// - /// Updates the specified information. - /// - /// The information. - /// Task. - void Update(AuthenticationInfo info); - - /// - /// Gets the specified query. - /// - /// The query. - /// QueryResult{AuthenticationInfo}. - QueryResult Get(AuthenticationInfoQuery query); - - void Delete(AuthenticationInfo info); - - DeviceOptions GetDeviceOptions(string deviceId); - - void UpdateDeviceOptions(string deviceId, DeviceOptions options); - } -} diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 30a83d6e73..1000da2473 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -6,10 +6,12 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Events; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Security; +using MediaBrowser.Model.Devices; using MediaBrowser.Model.Session; using MediaBrowser.Model.SyncPlay; @@ -325,26 +327,23 @@ namespace MediaBrowser.Controller.Session /// The remote endpoint. /// The application version. /// Task<SessionInfo>. - Task GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion); + Task GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, string appVersion); /// /// Logouts the specified access token. /// /// The access token. - void Logout(string accessToken); + /// A representing the log out process. + Task Logout(string accessToken); - void Logout(AuthenticationInfo accessToken); + Task Logout(Device accessToken); /// /// Revokes the user tokens. /// - void RevokeUserTokens(Guid userId, string currentAccessToken); - - /// - /// Revokes the token. - /// - /// The identifier. - void RevokeToken(string id); + /// The user's id. + /// The current access token. + Task RevokeUserTokens(Guid userId, string currentAccessToken); void CloseIfNeeded(SessionInfo session); } diff --git a/MediaBrowser.Model/Devices/DeviceInfo.cs b/MediaBrowser.Model/Devices/DeviceInfo.cs index 0cccf931c4..7a1c7a7382 100644 --- a/MediaBrowser.Model/Devices/DeviceInfo.cs +++ b/MediaBrowser.Model/Devices/DeviceInfo.cs @@ -15,6 +15,11 @@ namespace MediaBrowser.Model.Devices public string Name { get; set; } + /// + /// Gets or sets the access token. + /// + public string AccessToken { get; set; } + /// /// Gets or sets the identifier. /// diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs index de03aa5f5b..cd03958b66 100644 --- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs @@ -136,7 +136,7 @@ namespace Jellyfin.Api.Tests.Auth _jellyfinAuthServiceMock.Setup( a => a.Authenticate( It.IsAny())) - .Returns(authorizationInfo); + .Returns(Task.FromResult(authorizationInfo)); return authorizationInfo; } From 37a8a82ac54a795402006b116a9ef5ece4a3eb6c Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 21 May 2021 00:19:54 -0400 Subject: [PATCH 20/82] Make methods static in AuthenticationContext --- .../Security/AuthorizationContext.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs index 688ff71682..b10b7a9985 100644 --- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs +++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs @@ -199,7 +199,7 @@ namespace Jellyfin.Server.Implementations.Security /// /// The HTTP req. /// Dictionary{System.StringSystem.String}. - private Dictionary? GetAuthorizationDictionary(HttpContext httpReq) + private static Dictionary? GetAuthorizationDictionary(HttpContext httpReq) { var auth = httpReq.Request.Headers["X-Emby-Authorization"]; @@ -216,7 +216,7 @@ namespace Jellyfin.Server.Implementations.Security /// /// The HTTP req. /// Dictionary{System.StringSystem.String}. - private Dictionary? GetAuthorizationDictionary(HttpRequest httpReq) + private static Dictionary? GetAuthorizationDictionary(HttpRequest httpReq) { var auth = httpReq.Headers["X-Emby-Authorization"]; @@ -233,7 +233,7 @@ namespace Jellyfin.Server.Implementations.Security /// /// The authorization header. /// Dictionary{System.StringSystem.String}. - private Dictionary? GetAuthorization(ReadOnlySpan authorizationHeader) + private static Dictionary? GetAuthorization(ReadOnlySpan authorizationHeader) { var firstSpace = authorizationHeader.IndexOf(' '); From 373155a06324140fe8b0ff95d4f58c7a498db2e2 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 18 Jun 2021 16:57:46 -0400 Subject: [PATCH 21/82] Correctly handle devices without custom names --- Emby.Server.Implementations/Session/SessionManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 92ef65bf11..b8c778690b 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -532,7 +532,7 @@ namespace Emby.Server.Implementations.Session } var deviceOptions = await _deviceManager.GetDeviceOptions(deviceId).ConfigureAwait(false); - if (string.IsNullOrEmpty(deviceOptions.CustomName)) + if (string.IsNullOrEmpty(deviceOptions?.CustomName)) { sessionInfo.DeviceName = deviceName; } From 3d9c16ba6b8be965da8ed57510f8f90831ed1e22 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 18 Jun 2021 17:06:38 -0400 Subject: [PATCH 22/82] Fix concurrency issues --- .../Security/AuthorizationContext.cs | 13 +++++++------ Jellyfin.Server/CoreAppHost.cs | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs index b10b7a9985..92be5477cb 100644 --- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs +++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs @@ -15,12 +15,12 @@ namespace Jellyfin.Server.Implementations.Security { public class AuthorizationContext : IAuthorizationContext { - private readonly JellyfinDb _jellyfinDb; + private readonly JellyfinDbProvider _jellyfinDbProvider; private readonly IUserManager _userManager; - public AuthorizationContext(JellyfinDb jellyfinDb, IUserManager userManager) + public AuthorizationContext(JellyfinDbProvider jellyfinDb, IUserManager userManager) { - _jellyfinDb = jellyfinDb; + _jellyfinDbProvider = jellyfinDb; _userManager = userManager; } @@ -117,7 +117,8 @@ namespace Jellyfin.Server.Implementations.Security #pragma warning restore CA1508 authInfo.HasToken = true; - var device = await _jellyfinDb.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false); + await using var jellyfinDb = _jellyfinDbProvider.CreateContext(); + var device = await jellyfinDb.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false); if (device != null) { @@ -186,8 +187,8 @@ namespace Jellyfin.Server.Implementations.Security if (updateToken) { - _jellyfinDb.Devices.Update(device); - await _jellyfinDb.SaveChangesAsync().ConfigureAwait(false); + jellyfinDb.Devices.Update(device); + await jellyfinDb.SaveChangesAsync().ConfigureAwait(false); } } diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 362be85316..e480b9a69a 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -95,7 +95,7 @@ namespace Jellyfin.Server ServiceCollection.AddSingleton(); ServiceCollection.AddSingleton(); - ServiceCollection.AddScoped(); + ServiceCollection.AddSingleton(); base.RegisterServices(); } From 3fd0b1a359241b8d5d4884424b72b5c1dc1caed0 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 18 Jun 2021 17:07:22 -0400 Subject: [PATCH 23/82] Recreate devices migration with missing fields --- Jellyfin.Data/Entities/Security/ApiKey.cs | 9 +++++++-- Jellyfin.Data/Entities/Security/Device.cs | 4 ++-- ...Designer.cs => 20210602224232_AddDevices.Designer.cs} | 8 ++++++-- ...032224_AddDevices.cs => 20210602224232_AddDevices.cs} | 2 ++ .../Migrations/JellyfinDbModelSnapshot.cs | 5 ++++- 5 files changed, 21 insertions(+), 7 deletions(-) rename Jellyfin.Server.Implementations/Migrations/{20210521032224_AddDevices.Designer.cs => 20210602224232_AddDevices.Designer.cs} (99%) rename Jellyfin.Server.Implementations/Migrations/{20210521032224_AddDevices.cs => 20210602224232_AddDevices.cs} (98%) diff --git a/Jellyfin.Data/Entities/Security/ApiKey.cs b/Jellyfin.Data/Entities/Security/ApiKey.cs index 2a3ad09c43..5c9ac5d5b8 100644 --- a/Jellyfin.Data/Entities/Security/ApiKey.cs +++ b/Jellyfin.Data/Entities/Security/ApiKey.cs @@ -31,9 +31,14 @@ namespace Jellyfin.Data.Entities.Security public int Id { get; private set; } /// - /// Gets the date created. + /// Gets or sets the date created. /// - public DateTime DateCreated { get; private set; } + public DateTime DateCreated { get; set; } + + /// + /// Gets or sets the date of last activity. + /// + public DateTime DateLastActivity { get; set; } /// /// Gets or sets the name. diff --git a/Jellyfin.Data/Entities/Security/Device.cs b/Jellyfin.Data/Entities/Security/Device.cs index bb05cf5a4f..3d0269229d 100644 --- a/Jellyfin.Data/Entities/Security/Device.cs +++ b/Jellyfin.Data/Entities/Security/Device.cs @@ -84,9 +84,9 @@ namespace Jellyfin.Data.Entities.Security public bool IsActive { get; set; } /// - /// Gets the date this device was created. + /// Gets or sets the date this device was created. /// - public DateTime DateCreated { get; private set; } + public DateTime DateCreated { get; set; } /// /// Gets or sets the date of last activity. diff --git a/Jellyfin.Server.Implementations/Migrations/20210521032224_AddDevices.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20210602224232_AddDevices.Designer.cs similarity index 99% rename from Jellyfin.Server.Implementations/Migrations/20210521032224_AddDevices.Designer.cs rename to Jellyfin.Server.Implementations/Migrations/20210602224232_AddDevices.Designer.cs index e1faef7a2f..933e82229d 100644 --- a/Jellyfin.Server.Implementations/Migrations/20210521032224_AddDevices.Designer.cs +++ b/Jellyfin.Server.Implementations/Migrations/20210602224232_AddDevices.Designer.cs @@ -1,4 +1,5 @@ #pragma warning disable CS1591 + // using System; using Jellyfin.Server.Implementations; @@ -10,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Jellyfin.Server.Implementations.Migrations { [DbContext(typeof(JellyfinDb))] - [Migration("20210521032224_AddDevices")] + [Migration("20210602224232_AddDevices")] partial class AddDevices { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -18,7 +19,7 @@ namespace Jellyfin.Server.Implementations.Migrations #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("jellyfin") - .HasAnnotation("ProductVersion", "5.0.5"); + .HasAnnotation("ProductVersion", "5.0.6"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -347,6 +348,9 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property("DateCreated") .HasColumnType("TEXT"); + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + b.Property("Name") .IsRequired() .HasMaxLength(64) diff --git a/Jellyfin.Server.Implementations/Migrations/20210521032224_AddDevices.cs b/Jellyfin.Server.Implementations/Migrations/20210602224232_AddDevices.cs similarity index 98% rename from Jellyfin.Server.Implementations/Migrations/20210521032224_AddDevices.cs rename to Jellyfin.Server.Implementations/Migrations/20210602224232_AddDevices.cs index 2da8d37881..110ca1cc39 100644 --- a/Jellyfin.Server.Implementations/Migrations/20210521032224_AddDevices.cs +++ b/Jellyfin.Server.Implementations/Migrations/20210602224232_AddDevices.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 #pragma warning disable SA1601 + using System; using Microsoft.EntityFrameworkCore.Migrations; @@ -17,6 +18,7 @@ namespace Jellyfin.Server.Implementations.Migrations Id = table.Column(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), DateCreated = table.Column(type: "TEXT", nullable: false), + DateLastActivity = table.Column(type: "TEXT", nullable: false), Name = table.Column(type: "TEXT", maxLength: 64, nullable: false), AccessToken = table.Column(type: "TEXT", nullable: false) }, diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index 8a1ae16f84..b508b834ce 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", "5.0.5"); + .HasAnnotation("ProductVersion", "5.0.6"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -344,6 +344,9 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property("DateCreated") .HasColumnType("TEXT"); + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + b.Property("Name") .IsRequired() .HasMaxLength(64) From 32645750474efeb13c3d32f7fed0dd5db328d6ac Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 18 Jun 2021 17:07:30 -0400 Subject: [PATCH 24/82] Add data migration --- Jellyfin.Server/Migrations/MigrationRunner.cs | 3 +- .../Routines/MigrateAuthenticationDb.cs | 105 ++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index cf938ab8cd..2710f94af3 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -25,7 +25,8 @@ namespace Jellyfin.Server.Migrations typeof(Routines.ReaddDefaultPluginRepository), typeof(Routines.MigrateDisplayPreferencesDb), typeof(Routines.RemoveDownloadImagesInAdvance), - typeof(Routines.AddPeopleQueryIndex) + typeof(Routines.AddPeopleQueryIndex), + typeof(Routines.MigrateAuthenticationDb) }; /// diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs new file mode 100644 index 0000000000..10afc52a19 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs @@ -0,0 +1,105 @@ +using System; +using System.IO; +using Emby.Server.Implementations.Data; +using Jellyfin.Data.Entities.Security; +using Jellyfin.Server.Implementations; +using MediaBrowser.Controller; +using Microsoft.Extensions.Logging; +using SQLitePCL.pretty; + +namespace Jellyfin.Server.Migrations.Routines +{ + /// + /// A migration that moves data from the authentication database into the new schema. + /// + public class MigrateAuthenticationDb : IMigrationRoutine + { + private const string DbFilename = "authentication.db"; + + private readonly ILogger _logger; + private readonly JellyfinDbProvider _dbProvider; + private readonly IServerApplicationPaths _appPaths; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The database provider. + /// The server application paths. + public MigrateAuthenticationDb(ILogger logger, JellyfinDbProvider dbProvider, IServerApplicationPaths appPaths) + { + _logger = logger; + _dbProvider = dbProvider; + _appPaths = appPaths; + } + + /// + public Guid Id => Guid.Parse("5BD72F41-E6F3-4F60-90AA-09869ABE0E22"); + + /// + public string Name => "MigrateAuthenticationDatabase"; + + /// + public bool PerformOnNewInstall => false; + + /// + public void Perform() + { + var dataPath = _appPaths.DataPath; + using (var connection = SQLite3.Open( + Path.Combine(dataPath, DbFilename), + ConnectionFlags.ReadOnly, + null)) + { + using var dbContext = _dbProvider.CreateContext(); + + var queryResult = connection.Query("SELECT * FROM Tokens"); + + foreach (var row in queryResult) + { + if (row[6].IsDbNull()) + { + dbContext.ApiKeys.Add(new ApiKey(row[3].ToString()) + { + AccessToken = row[1].ToGuid(), + DateCreated = row[9].ToDateTime(), + DateLastActivity = row[10].ToDateTime() + }); + } + else + { + dbContext.Devices.Add(new Device( + row[6].ToGuid(), + row[3].ToString(), + row[4].ToString(), + row[5].ToString(), + row[2].ToString()) + { + AccessToken = row[1].ToString(), + IsActive = row[8].ToBool(), + DateCreated = row[9].ToDateTime(), + DateLastActivity = row[10].ToDateTime() + }); + } + } + + dbContext.SaveChanges(); + } + + try + { + 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 activity log database to 'authentication.db.old'"); + } + } + } +} From 4206c0e0910e257f2fa05db7e63b554919d799b3 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 18 Jun 2021 17:09:39 -0400 Subject: [PATCH 25/82] Combine if statements --- .../Security/AuthorizationContext.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs index 92be5477cb..4b073ed33e 100644 --- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs +++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs @@ -123,10 +123,6 @@ namespace Jellyfin.Server.Implementations.Security if (device != null) { authInfo.IsAuthenticated = true; - } - - if (device != null) - { var updateToken = false; // TODO: Remove these checks for IsNullOrWhiteSpace From 0292936c659b25464c1bc1e1b80711f873a1a7cd Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 18 Jun 2021 17:09:59 -0400 Subject: [PATCH 26/82] Use consistent name for db context --- .../Security/AuthorizationContext.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs index 4b073ed33e..e589fae301 100644 --- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs +++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs @@ -117,8 +117,8 @@ namespace Jellyfin.Server.Implementations.Security #pragma warning restore CA1508 authInfo.HasToken = true; - await using var jellyfinDb = _jellyfinDbProvider.CreateContext(); - var device = await jellyfinDb.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false); + await using var dbContext = _jellyfinDbProvider.CreateContext(); + var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false); if (device != null) { @@ -183,8 +183,8 @@ namespace Jellyfin.Server.Implementations.Security if (updateToken) { - jellyfinDb.Devices.Update(device); - await jellyfinDb.SaveChangesAsync().ConfigureAwait(false); + dbContext.Devices.Update(device); + await dbContext.SaveChangesAsync().ConfigureAwait(false); } } From 336ba2879f325a4efd52bc7737ce94f40369bfeb Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 18 Jun 2021 18:26:58 -0400 Subject: [PATCH 27/82] Re-add support for API keys --- Jellyfin.Api/Controllers/ApiKeyController.cs | 2 +- Jellyfin.Data/Entities/Security/ApiKey.cs | 5 +++-- .../Security/AuthenticationManager.cs | 8 +++----- .../Security/AuthorizationContext.cs | 13 +++++++++++++ .../Migrations/Routines/MigrateAuthenticationDb.cs | 2 +- .../Security/IAuthenticationManager.cs | 2 +- 6 files changed, 22 insertions(+), 10 deletions(-) diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs index 96efde5fbd..720b22b1d6 100644 --- a/Jellyfin.Api/Controllers/ApiKeyController.cs +++ b/Jellyfin.Api/Controllers/ApiKeyController.cs @@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers [HttpDelete("Keys/{key}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task RevokeKey([FromRoute, Required] Guid key) + public async Task RevokeKey([FromRoute, Required] string key) { await _authenticationManager.DeleteApiKey(key).ConfigureAwait(false); diff --git a/Jellyfin.Data/Entities/Security/ApiKey.cs b/Jellyfin.Data/Entities/Security/ApiKey.cs index 5c9ac5d5b8..31d865d018 100644 --- a/Jellyfin.Data/Entities/Security/ApiKey.cs +++ b/Jellyfin.Data/Entities/Security/ApiKey.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using System.Globalization; namespace Jellyfin.Data.Entities.Security { @@ -17,7 +18,7 @@ namespace Jellyfin.Data.Entities.Security { Name = name; - AccessToken = Guid.NewGuid(); + AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); DateCreated = DateTime.UtcNow; } @@ -50,6 +51,6 @@ namespace Jellyfin.Data.Entities.Security /// /// Gets or sets the access token. /// - public Guid AccessToken { get; set; } + public string AccessToken { get; set; } } } diff --git a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs index ab76e2302f..b79e46469c 100644 --- a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs +++ b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Globalization; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Jellyfin.Data.Entities.Security; @@ -43,7 +41,7 @@ namespace Jellyfin.Server.Implementations.Security .Select(key => new AuthenticationInfo { AppName = key.Name, - AccessToken = key.AccessToken.ToString("N", CultureInfo.InvariantCulture), + AccessToken = key.AccessToken, DateCreated = key.DateCreated, DeviceId = string.Empty, DeviceName = string.Empty, @@ -52,7 +50,7 @@ namespace Jellyfin.Server.Implementations.Security } /// - public async Task DeleteApiKey(Guid accessToken) + public async Task DeleteApiKey(string accessToken) { await using var dbContext = _dbProvider.CreateContext(); diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs index e589fae301..9a073c4770 100644 --- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs +++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs @@ -187,6 +187,19 @@ namespace Jellyfin.Server.Implementations.Security await dbContext.SaveChangesAsync().ConfigureAwait(false); } } + else + { + var key = await dbContext.ApiKeys.FirstOrDefaultAsync(apiKey => apiKey.AccessToken == token).ConfigureAwait(false); + if (key != null) + { + authInfo.IsAuthenticated = true; + authInfo.Client = key.Name; + authInfo.Token = key.AccessToken; + authInfo.DeviceId = string.Empty; + authInfo.Device = string.Empty; + authInfo.Version = string.Empty; + } + } return authInfo; } diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs index 10afc52a19..9bcf245d38 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs @@ -61,7 +61,7 @@ namespace Jellyfin.Server.Migrations.Routines { dbContext.ApiKeys.Add(new ApiKey(row[3].ToString()) { - AccessToken = row[1].ToGuid(), + AccessToken = row[1].ToString(), DateCreated = row[9].ToDateTime(), DateLastActivity = row[10].ToDateTime() }); diff --git a/MediaBrowser.Controller/Security/IAuthenticationManager.cs b/MediaBrowser.Controller/Security/IAuthenticationManager.cs index 46d0c66224..29621b73e7 100644 --- a/MediaBrowser.Controller/Security/IAuthenticationManager.cs +++ b/MediaBrowser.Controller/Security/IAuthenticationManager.cs @@ -29,6 +29,6 @@ namespace MediaBrowser.Controller.Security /// /// The access token. /// A task representing the deletion of the API key. - Task DeleteApiKey(Guid accessToken); + Task DeleteApiKey(string accessToken); } } From 54fdc2fd889cd8c13f327b3e929482647a54329b Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 18 Jun 2021 19:15:08 -0400 Subject: [PATCH 28/82] Use ExecuteSqlInterpolated instead of ExecuteSqlRaw oop --- Jellyfin.Server.Implementations/Devices/DeviceManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index 9fd2e5ad4c..6bdf7b84ec 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -50,7 +50,7 @@ namespace Jellyfin.Server.Implementations.Devices { await using var dbContext = _dbProvider.CreateContext(); await dbContext.Database - .ExecuteSqlRawAsync($"UPDATE [DeviceOptions] SET [CustomName] = ${options.CustomName}") + .ExecuteSqlInterpolatedAsync($"UPDATE [DeviceOptions] SET [CustomName] = {options.CustomName}") .ConfigureAwait(false); DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs>(new Tuple(deviceId, options))); From 3123ea2a9489caec908eb7932d73fa586235ab91 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sat, 19 Jun 2021 15:09:16 -0400 Subject: [PATCH 29/82] Add missing ConfigureAwait call --- Jellyfin.Api/Controllers/DevicesController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 8af7b8f734..d4c2dbba0f 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -47,7 +47,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) { - return await _deviceManager.GetDevicesForUser(userId, supportsSync); + return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false); } /// From 6b24cc6d1f9699f246f4f136b42ec583229ad0e2 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sat, 19 Jun 2021 15:24:26 -0400 Subject: [PATCH 30/82] Fix UpdateDeviceOptions --- .../Devices/DeviceManager.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index 6bdf7b84ec..ab31276698 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -49,9 +49,15 @@ namespace Jellyfin.Server.Implementations.Devices public async Task UpdateDeviceOptions(string deviceId, DeviceOptions options) { await using var dbContext = _dbProvider.CreateContext(); - await dbContext.Database - .ExecuteSqlInterpolatedAsync($"UPDATE [DeviceOptions] SET [CustomName] = {options.CustomName}") - .ConfigureAwait(false); + var deviceOptions = await dbContext.DeviceOptions.AsQueryable().FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false); + if (deviceOptions == null) + { + deviceOptions = new DeviceOptions(deviceId); + dbContext.DeviceOptions.Add(deviceOptions); + } + + deviceOptions.CustomName = options.CustomName; + await dbContext.SaveChangesAsync().ConfigureAwait(false); DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs>(new Tuple(deviceId, options))); } From 2a9474f6e706624ce93883b4cb7616937e205f76 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sat, 19 Jun 2021 15:24:42 -0400 Subject: [PATCH 31/82] Count records before skipping --- Jellyfin.Server.Implementations/Devices/DeviceManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index ab31276698..b87245f0b0 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -130,13 +130,13 @@ namespace Jellyfin.Server.Implementations.Devices devices = devices.Where(device => device.AccessToken == query.AccessToken); } + var count = await devices.CountAsync().ConfigureAwait(false); + if (query.Skip.HasValue) { devices = devices.Skip(query.Skip.Value); } - var count = await devices.CountAsync().ConfigureAwait(false); - if (query.Limit.HasValue) { devices = devices.Take(query.Limit.Value); From 67308f489f8b321aa00755e4c74ce55c54928bc7 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sat, 19 Jun 2021 15:24:53 -0400 Subject: [PATCH 32/82] Implement DeleteDevice --- Jellyfin.Server.Implementations/Devices/DeviceManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index b87245f0b0..638ed24a23 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -194,6 +194,8 @@ namespace Jellyfin.Server.Implementations.Devices public async Task DeleteDevice(Device device) { await using var dbContext = _dbProvider.CreateContext(); + dbContext.Devices.Remove(device); + await dbContext.SaveChangesAsync().ConfigureAwait(false); } /// From d64e14fcb84112135f2a4d4ac1953eb37028885e Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sat, 19 Jun 2021 15:26:24 -0400 Subject: [PATCH 33/82] Use ReadOnlySpan in GetImagesForPrograms --- .../LiveTv/Listings/SchedulesDirect.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index cbdc7c7071..d26aed94f6 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -459,7 +459,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings } StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13)); - foreach (string i in programIds) + foreach (ReadOnlySpan i in programIds) { str.Append('"') .Append(i[..10]) @@ -789,7 +789,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings { var channelNumber = GetChannelNumber(channel); - var station = allStations.Find(item => string.Equals(item.stationID, channel.stationID, StringComparison.OrdinalIgnoreCase)) + var station = allStations.Find(item => string.Equals(item.stationID, channel.stationID, StringComparison.OrdinalIgnoreCase)) ?? new ScheduleDirect.Station { stationID = channel.stationID From 544e0593957e13a7597971b702379e7f4f6e9b3d Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Mon, 21 Jun 2021 18:57:59 -0400 Subject: [PATCH 34/82] Revert unintended change in SchedulesDirect --- Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index d26aed94f6..bd4b5639cd 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -462,7 +462,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings foreach (ReadOnlySpan i in programIds) { str.Append('"') - .Append(i[..10]) + .Append(i.Slice(0, 10)) .Append("\","); } From 784f29f75387d4a98b20103a59d94dc1994516d1 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Mon, 21 Jun 2021 19:00:24 -0400 Subject: [PATCH 35/82] Use named tuple for QuickConnect --- .../QuickConnect/QuickConnectManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs index b32c70eb91..783185e7de 100644 --- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs +++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs @@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.QuickConnect { private readonly RNGCryptoServiceProvider _rng = new (); private readonly ConcurrentDictionary _currentRequests = new (); - private readonly ConcurrentDictionary _quickConnectTokens = new (); + private readonly ConcurrentDictionary _quickConnectTokens = new (); private readonly IServerConfigurationManager _config; private readonly ILogger _logger; @@ -137,7 +137,7 @@ namespace Emby.Server.Implementations.QuickConnect throw new SecurityException("Unknown quick connect token"); } - request.UserId = entry.Item2; + request.UserId = entry.UserId; _quickConnectTokens.Remove(token, out _); _sessionManager.AuthenticateQuickConnect(request, token); @@ -195,7 +195,7 @@ namespace Emby.Server.Implementations.QuickConnect public int DeleteAllDevices(Guid user) { var tokens = _quickConnectTokens - .Where(entry => entry.Value.Item1.StartsWith(TokenName, StringComparison.Ordinal) && entry.Value.Item2 == user) + .Where(entry => entry.Value.Token.StartsWith(TokenName, StringComparison.Ordinal) && entry.Value.UserId == user) .ToList(); var removed = 0; From 3884837513bd4b658942c2a73186e869fbf1eee3 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Mon, 21 Jun 2021 19:01:34 -0400 Subject: [PATCH 36/82] Convert method group to lambda in DeviceManager --- Jellyfin.Server.Implementations/Devices/DeviceManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index 638ed24a23..484a53428c 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -157,7 +157,7 @@ namespace Jellyfin.Server.Implementations.Devices return new QueryResult { - Items = devices.Items.Select(ToDeviceInfo).ToList(), + Items = devices.Items.Select(device => ToDeviceInfo(device)).ToList(), StartIndex = devices.StartIndex, TotalRecordCount = devices.TotalRecordCount }; @@ -185,7 +185,7 @@ namespace Jellyfin.Server.Implementations.Devices sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId)); } - var array = await sessions.Select(ToDeviceInfo).ToArrayAsync().ConfigureAwait(false); + var array = await sessions.Select(device => ToDeviceInfo(device)).ToArrayAsync().ConfigureAwait(false); return new QueryResult(array); } From f96722fa749b94b8affbf75da5d6941cab219a84 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Mon, 21 Jun 2021 19:06:21 -0400 Subject: [PATCH 37/82] Fix migration --- Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs index 9bcf245d38..6c9ad03389 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs @@ -69,7 +69,7 @@ namespace Jellyfin.Server.Migrations.Routines else { dbContext.Devices.Add(new Device( - row[6].ToGuid(), + new Guid(row[6].ToString()), row[3].ToString(), row[4].ToString(), row[5].ToString(), From 397868be95db2f705522cc975ac076e60decbf0f Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 23 Jun 2021 21:07:08 -0600 Subject: [PATCH 38/82] Fix issues with QuickConnect and AuthenticationDb --- .../QuickConnect/QuickConnectManager.cs | 85 ++++++++++++++++--- .../Session/SessionManager.cs | 13 ++- .../Controllers/QuickConnectController.cs | 11 ++- Jellyfin.Api/Controllers/UserController.cs | 23 ++--- .../Models/UserDtos/QuickConnectDto.cs | 4 +- .../QuickConnect/IQuickConnect.cs | 13 ++- .../Session/ISessionManager.cs | 7 +- .../QuickConnect/QuickConnectResult.cs | 40 +++++++-- 8 files changed, 148 insertions(+), 48 deletions(-) diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs index afc08fc261..ae773c6589 100644 --- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs +++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; using MediaBrowser.Controller.QuickConnect; using MediaBrowser.Controller.Session; using MediaBrowser.Model.QuickConnect; @@ -29,8 +30,9 @@ namespace Emby.Server.Implementations.QuickConnect /// private const int Timeout = 10; - private readonly RNGCryptoServiceProvider _rng = new(); - private readonly ConcurrentDictionary _currentRequests = new(); + private readonly RNGCryptoServiceProvider _rng = new (); + private readonly ConcurrentDictionary _currentRequests = new (); + private readonly ConcurrentDictionary _authorizedSecrets = new (); private readonly IServerConfigurationManager _config; private readonly ILogger _logger; @@ -68,14 +70,41 @@ namespace Emby.Server.Implementations.QuickConnect } /// - public QuickConnectResult TryConnect() + public QuickConnectResult TryConnect(AuthorizationInfo authorizationInfo) { + if (string.IsNullOrEmpty(authorizationInfo.DeviceId)) + { + throw new ArgumentException(nameof(authorizationInfo.DeviceId) + " is required"); + } + + if (string.IsNullOrEmpty(authorizationInfo.Device)) + { + throw new ArgumentException(nameof(authorizationInfo.Device) + " is required"); + } + + if (string.IsNullOrEmpty(authorizationInfo.Client)) + { + throw new ArgumentException(nameof(authorizationInfo.Client) + " is required"); + } + + if (string.IsNullOrEmpty(authorizationInfo.Version)) + { + throw new ArgumentException(nameof(authorizationInfo.Version) + "is required"); + } + AssertActive(); ExpireRequests(); var secret = GenerateSecureRandom(); var code = GenerateCode(); - var result = new QuickConnectResult(secret, code, DateTime.UtcNow); + var result = new QuickConnectResult( + secret, + code, + DateTime.UtcNow, + authorizationInfo.DeviceId, + authorizationInfo.Device, + authorizationInfo.Client, + authorizationInfo.Version); _currentRequests[code] = result; return result; @@ -135,19 +164,41 @@ namespace Emby.Server.Implementations.QuickConnect throw new InvalidOperationException("Request is already authorized"); } - var token = Guid.NewGuid(); - result.Authentication = token; - // Change the time on the request so it expires one minute into the future. It can't expire immediately as otherwise some clients wouldn't ever see that they have been authenticated. - result.DateAdded = DateTime.Now.Add(TimeSpan.FromMinutes(1)); + result.DateAdded = DateTime.UtcNow.Add(TimeSpan.FromMinutes(1)); - await _sessionManager.AuthenticateQuickConnect(userId).ConfigureAwait(false); + var authenticationResult = await _sessionManager.AuthenticateDirect(new AuthenticationRequest + { + UserId = userId, + DeviceId = result.DeviceId, + DeviceName = result.DeviceName, + App = result.AppName, + AppVersion = result.AppVersion + }).ConfigureAwait(false); - _logger.LogDebug("Authorizing device with code {Code} to login as user {userId}", code, userId); + _authorizedSecrets[result.Secret] = (DateTime.UtcNow, authenticationResult); + result.Authenticated = true; + _currentRequests[code] = result; + + _logger.LogDebug("Authorizing device with code {Code} to login as user {UserId}", code, userId); return true; } + /// + public AuthenticationResult GetAuthorizedRequest(string secret) + { + AssertActive(); + ExpireRequests(); + + if (!_authorizedSecrets.TryGetValue(secret, out var result)) + { + throw new ResourceNotFoundException("Unable to find request"); + } + + return result.AuthenticationResult; + } + /// /// Dispose. /// @@ -189,7 +240,7 @@ namespace Emby.Server.Implementations.QuickConnect // Expire stale connection requests foreach (var (_, currentRequest) in _currentRequests) { - if (expireAll || currentRequest.DateAdded > minTime) + if (expireAll || currentRequest.DateAdded < minTime) { var code = currentRequest.Code; _logger.LogDebug("Removing expired request {Code}", code); @@ -200,6 +251,18 @@ namespace Emby.Server.Implementations.QuickConnect } } } + + foreach (var (secret, (timestamp, _)) in _authorizedSecrets) + { + if (expireAll || timestamp < minTime) + { + _logger.LogDebug("Removing expired secret {Secret}", secret); + if (!_authorizedSecrets.TryRemove(secret, out _)) + { + _logger.LogWarning("Secret {Secret} already expired", secret); + } + } + } } } } diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 29b545583a..40a346e95e 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1432,16 +1432,21 @@ namespace Emby.Server.Implementations.Session /// /// Authenticates the new session. /// - /// The request. - /// Task{SessionInfo}. + /// The authenticationrequest. + /// The authentication result. public Task AuthenticateNewSession(AuthenticationRequest request) { return AuthenticateNewSessionInternal(request, true); } - public Task AuthenticateQuickConnect(Guid userId) + /// + /// Directly authenticates the session without enforcing password. + /// + /// The authentication request. + /// The authentication result. + public Task AuthenticateDirect(AuthenticationRequest request) { - return AuthenticateNewSessionInternal(new AuthenticationRequest { UserId = userId }, false); + return AuthenticateNewSessionInternal(request, false); } private async Task AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword) diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs index 56fef08a99..87b78fe93f 100644 --- a/Jellyfin.Api/Controllers/QuickConnectController.cs +++ b/Jellyfin.Api/Controllers/QuickConnectController.cs @@ -4,6 +4,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Net; using MediaBrowser.Controller.QuickConnect; using MediaBrowser.Model.QuickConnect; using Microsoft.AspNetCore.Authorization; @@ -18,14 +19,17 @@ namespace Jellyfin.Api.Controllers public class QuickConnectController : BaseJellyfinApiController { private readonly IQuickConnect _quickConnect; + private readonly IAuthorizationContext _authContext; /// /// Initializes a new instance of the class. /// /// Instance of the interface. - public QuickConnectController(IQuickConnect quickConnect) + /// Instance of the interface. + public QuickConnectController(IQuickConnect quickConnect, IAuthorizationContext authContext) { _quickConnect = quickConnect; + _authContext = authContext; } /// @@ -48,11 +52,12 @@ namespace Jellyfin.Api.Controllers /// A with a secret and code for future use or an error message. [HttpGet("Initiate")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult Initiate() + public async Task> Initiate() { try { - return _quickConnect.TryConnect(); + var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); + return _quickConnect.TryConnect(auth); } catch (AuthenticationException) { diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 8e2298bb7d..4263d4fe5f 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -14,6 +14,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.QuickConnect; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; @@ -38,6 +39,7 @@ namespace Jellyfin.Api.Controllers private readonly IAuthorizationContext _authContext; private readonly IServerConfigurationManager _config; private readonly ILogger _logger; + private readonly IQuickConnect _quickConnectManager; /// /// Initializes a new instance of the class. @@ -49,6 +51,7 @@ namespace Jellyfin.Api.Controllers /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. + /// Instance of the interface. public UserController( IUserManager userManager, ISessionManager sessionManager, @@ -56,7 +59,8 @@ namespace Jellyfin.Api.Controllers IDeviceManager deviceManager, IAuthorizationContext authContext, IServerConfigurationManager config, - ILogger logger) + ILogger logger, + IQuickConnect quickConnectManager) { _userManager = userManager; _sessionManager = sessionManager; @@ -65,6 +69,7 @@ namespace Jellyfin.Api.Controllers _authContext = authContext; _config = config; _logger = logger; + _quickConnectManager = quickConnectManager; } /// @@ -228,23 +233,11 @@ namespace Jellyfin.Api.Controllers /// A containing an with information about the new session. [HttpPost("AuthenticateWithQuickConnect")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request) + public ActionResult AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request) { - var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); - try { - var authRequest = new AuthenticationRequest - { - App = auth.Client, - AppVersion = auth.Version, - DeviceId = auth.DeviceId, - DeviceName = auth.Device, - }; - - return await _sessionManager.AuthenticateQuickConnect( - authRequest, - request.Token).ConfigureAwait(false); + return _quickConnectManager.GetAuthorizedRequest(request.Secret); } catch (SecurityException e) { diff --git a/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs b/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs index c3a2d5cec2..9493c08c28 100644 --- a/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs +++ b/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs @@ -8,9 +8,9 @@ namespace Jellyfin.Api.Models.UserDtos public class QuickConnectDto { /// - /// Gets or sets the quick connect token. + /// Gets or sets the quick connect secret. /// [Required] - public string? Token { get; set; } + public string Secret { get; set; } = null!; } } diff --git a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs index 616409533e..ec3706773a 100644 --- a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs +++ b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; -using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Net; using MediaBrowser.Model.QuickConnect; namespace MediaBrowser.Controller.QuickConnect @@ -18,8 +19,9 @@ namespace MediaBrowser.Controller.QuickConnect /// /// Initiates a new quick connect request. /// + /// The initiator authorization info. /// A quick connect result with tokens to proceed or throws an exception if not active. - QuickConnectResult TryConnect(); + QuickConnectResult TryConnect(AuthorizationInfo authorizationInfo); /// /// Checks the status of an individual request. @@ -35,5 +37,12 @@ namespace MediaBrowser.Controller.QuickConnect /// Identifying code for the request. /// A boolean indicating if the authorization completed successfully. Task AuthorizeRequest(Guid userId, string code); + + /// + /// Gets the authorized request for the secret. + /// + /// The secret. + /// The authentication result. + AuthenticationResult GetAuthorizedRequest(string secret); } } diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 8be9ff5219..88a905166f 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -273,12 +273,7 @@ namespace MediaBrowser.Controller.Session /// Task{SessionInfo}. Task AuthenticateNewSession(AuthenticationRequest request); - /// - /// Authenticates a new session with quick connect. - /// - /// The user id. - /// Task{SessionInfo}. - Task AuthenticateQuickConnect(Guid userId); + Task AuthenticateDirect(AuthenticationRequest request); /// /// Reports the capabilities. diff --git a/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs b/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs index d180d29860..35a82f47cd 100644 --- a/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs +++ b/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs @@ -13,17 +13,32 @@ namespace MediaBrowser.Model.QuickConnect /// The secret used to query the request state. /// The code used to allow the request. /// The time when the request was created. - public QuickConnectResult(string secret, string code, DateTime dateAdded) + /// The requesting device id. + /// The requesting device name. + /// The requesting app name. + /// The requesting app version. + public QuickConnectResult( + string secret, + string code, + DateTime dateAdded, + string deviceId, + string deviceName, + string appName, + string appVersion) { Secret = secret; Code = code; DateAdded = dateAdded; + DeviceId = deviceId; + DeviceName = deviceName; + AppName = appName; + AppVersion = appVersion; } /// - /// Gets a value indicating whether this request is authorized. + /// Gets or sets a value indicating whether this request is authorized. /// - public bool Authenticated => Authentication != null; + public bool Authenticated { get; set; } /// /// Gets the secret value used to uniquely identify this request. Can be used to retrieve authentication information. @@ -36,9 +51,24 @@ namespace MediaBrowser.Model.QuickConnect public string Code { get; } /// - /// Gets or sets the private access token. + /// Gets the requesting device id. /// - public Guid? Authentication { get; set; } + public string DeviceId { get; } + + /// + /// Gets the requesting device name. + /// + public string DeviceName { get; } + + /// + /// Gets the requesting app name. + /// + public string AppName { get; } + + /// + /// Gets the requesting app version. + /// + public string AppVersion { get; } /// /// Gets or sets the DateTime that this request was created. From dbfd30ec4c33bb014d987e3bcb2e7a73a5389c15 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Thu, 24 Jun 2021 09:38:37 -0400 Subject: [PATCH 39/82] Migrate DeviceOptions --- .../Routines/MigrateAuthenticationDb.cs | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs index 6c9ad03389..21f153623e 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using Emby.Server.Implementations.Data; using Jellyfin.Data.Entities.Security; @@ -53,9 +54,9 @@ namespace Jellyfin.Server.Migrations.Routines { using var dbContext = _dbProvider.CreateContext(); - var queryResult = connection.Query("SELECT * FROM Tokens"); + var authenticatedDevices = connection.Query("SELECT * FROM Tokens"); - foreach (var row in queryResult) + foreach (var row in authenticatedDevices) { if (row[6].IsDbNull()) { @@ -83,6 +84,29 @@ namespace Jellyfin.Server.Migrations.Routines } } + var deviceOptions = connection.Query("SELECT * FROM Devices"); + var deviceIds = new HashSet(); + foreach (var row in deviceOptions) + { + if (row[2].IsDbNull()) + { + continue; + } + + var deviceId = row[2].ToString(); + if (deviceIds.Contains(deviceId)) + { + continue; + } + + deviceIds.Add(deviceId); + + dbContext.DeviceOptions.Add(new DeviceOptions(deviceId) + { + CustomName = row[1].IsDbNull() ? null : row[1].ToString() + }); + } + dbContext.SaveChanges(); } From befedaf6fc70456557de71b9d1d223f669e5c22b Mon Sep 17 00:00:00 2001 From: Patrick Barron <18354464+barronpm@users.noreply.github.com> Date: Fri, 25 Jun 2021 21:13:38 -0400 Subject: [PATCH 40/82] Update Emby.Server.Implementations/Session/SessionManager.cs Co-authored-by: Cody Robibero --- Emby.Server.Implementations/Session/SessionManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 40a346e95e..9dada44cba 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -293,7 +293,7 @@ namespace Emby.Server.Implementations.Session try { user.LastActivityDate = activityDate; - await _userManager.UpdateUserAsync(user); + await _userManager.UpdateUserAsync(user).ConfigureAwait(false); } catch (DbUpdateConcurrencyException e) { From 5d1139ec62ff0c177e45574a6c3ce7ecf51aff57 Mon Sep 17 00:00:00 2001 From: Patrick Barron <18354464+barronpm@users.noreply.github.com> Date: Sat, 26 Jun 2021 15:17:57 -0400 Subject: [PATCH 41/82] Update Jellyfin.Server.Implementations/Devices/DeviceManager.cs Co-authored-by: Cody Robibero --- Jellyfin.Server.Implementations/Devices/DeviceManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index 484a53428c..86706cac97 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -168,6 +168,7 @@ namespace Jellyfin.Server.Implementations.Devices { await using var dbContext = _dbProvider.CreateContext(); var sessions = dbContext.Devices + .Include(d => d.User) .AsQueryable() .OrderBy(d => d.DeviceId) .ThenByDescending(d => d.DateLastActivity) From bbac9ff67e60d243dbd05be60abfcf13c295cd84 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sun, 27 Jun 2021 16:42:26 -0400 Subject: [PATCH 42/82] GetDeviceOptions always returns an instance of DeviceOptions --- Emby.Server.Implementations/Session/SessionManager.cs | 2 +- Jellyfin.Api/Controllers/DevicesController.cs | 6 ------ Jellyfin.Server.Implementations/Devices/DeviceManager.cs | 6 ++++-- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 40a346e95e..ac730fa432 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -535,7 +535,7 @@ namespace Emby.Server.Implementations.Session } var deviceOptions = await _deviceManager.GetDeviceOptions(deviceId).ConfigureAwait(false); - if (string.IsNullOrEmpty(deviceOptions?.CustomName)) + if (string.IsNullOrEmpty(deviceOptions.CustomName)) { sessionInfo.DeviceName = deviceName; } diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index d4c2dbba0f..26b9a854da 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -107,12 +107,6 @@ namespace Jellyfin.Api.Controllers [FromQuery, Required] string id, [FromBody, Required] DeviceOptions deviceOptions) { - var existingDeviceOptions = await _deviceManager.GetDeviceOptions(id).ConfigureAwait(false); - if (existingDeviceOptions == null) - { - return NotFound(); - } - await _deviceManager.UpdateDeviceOptions(id, deviceOptions).ConfigureAwait(false); return NoContent(); } diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index 484a53428c..b02ca4ef0a 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -74,13 +74,15 @@ namespace Jellyfin.Server.Implementations.Devices } /// - public async Task GetDeviceOptions(string deviceId) + public async Task GetDeviceOptions(string deviceId) { await using var dbContext = _dbProvider.CreateContext(); - return await dbContext.DeviceOptions + var deviceOptions = await dbContext.DeviceOptions .AsQueryable() .FirstOrDefaultAsync(d => d.DeviceId == deviceId) .ConfigureAwait(false); + + return deviceOptions ?? new DeviceOptions(deviceId); } /// From fdba71e133f980cfbea3e198cf5236528e753fa2 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sun, 27 Jun 2021 16:44:34 -0400 Subject: [PATCH 43/82] Fix Api Key authentication --- .../Security/AuthorizationContext.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs index 08970f84de..8a5d513efa 100644 --- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs +++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs @@ -171,15 +171,7 @@ namespace Jellyfin.Server.Implementations.Security updateToken = true; } - if (!device.UserId.Equals(Guid.Empty)) - { - authInfo.User = _userManager.GetUserById(device.UserId); - authInfo.IsApiKey = false; - } - else - { - authInfo.IsApiKey = true; - } + authInfo.User = _userManager.GetUserById(device.UserId); if (updateToken) { @@ -198,6 +190,7 @@ namespace Jellyfin.Server.Implementations.Security authInfo.DeviceId = string.Empty; authInfo.Device = string.Empty; authInfo.Version = string.Empty; + authInfo.IsApiKey = true; } } From af2e7aec2ecff589c91b8064b1dff2ff5afcf172 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sun, 27 Jun 2021 16:45:41 -0400 Subject: [PATCH 44/82] Add missing service registration for IAuthenticationManager --- Jellyfin.Server/CoreAppHost.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index e480b9a69a..d41b5f74ec 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -20,6 +20,7 @@ using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Security; using MediaBrowser.Model.Activity; using MediaBrowser.Model.IO; using Microsoft.EntityFrameworkCore; @@ -97,6 +98,8 @@ namespace Jellyfin.Server ServiceCollection.AddSingleton(); + ServiceCollection.AddScoped(); + base.RegisterServices(); } From 60ce0c9fa9a3df50a8a7a08629bcedbe3724aee3 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 13 Jul 2021 19:30:11 -0400 Subject: [PATCH 45/82] Add dto for device options --- Jellyfin.Api/Controllers/DevicesController.cs | 5 ++-- Jellyfin.Data/Dtos/DeviceOptionsDto.cs | 23 +++++++++++++++++++ .../Devices/DeviceManager.cs | 6 ++--- .../Devices/IDeviceManager.cs | 2 +- 4 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 Jellyfin.Data/Dtos/DeviceOptionsDto.cs diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 26b9a854da..ebe7b7584b 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Data.Dtos; using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Queries; using MediaBrowser.Controller.Devices; @@ -105,9 +106,9 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task UpdateDeviceOptions( [FromQuery, Required] string id, - [FromBody, Required] DeviceOptions deviceOptions) + [FromBody, Required] DeviceOptionsDto deviceOptions) { - await _deviceManager.UpdateDeviceOptions(id, deviceOptions).ConfigureAwait(false); + await _deviceManager.UpdateDeviceOptions(id, deviceOptions.CustomName).ConfigureAwait(false); return NoContent(); } diff --git a/Jellyfin.Data/Dtos/DeviceOptionsDto.cs b/Jellyfin.Data/Dtos/DeviceOptionsDto.cs new file mode 100644 index 0000000000..392ef5ff4e --- /dev/null +++ b/Jellyfin.Data/Dtos/DeviceOptionsDto.cs @@ -0,0 +1,23 @@ +namespace Jellyfin.Data.Dtos +{ + /// + /// A dto representing custom options for a device. + /// + public class DeviceOptionsDto + { + /// + /// Gets or sets the id. + /// + public int Id { get; set; } + + /// + /// Gets or sets the device id. + /// + public string? DeviceId { get; set; } + + /// + /// Gets or sets the custom name. + /// + public string? CustomName { get; set; } + } +} diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index ef0d5db09f..3d1bc30e8a 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -46,7 +46,7 @@ namespace Jellyfin.Server.Implementations.Devices } /// - public async Task UpdateDeviceOptions(string deviceId, DeviceOptions options) + public async Task UpdateDeviceOptions(string deviceId, string deviceName) { await using var dbContext = _dbProvider.CreateContext(); var deviceOptions = await dbContext.DeviceOptions.AsQueryable().FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false); @@ -56,10 +56,10 @@ namespace Jellyfin.Server.Implementations.Devices dbContext.DeviceOptions.Add(deviceOptions); } - deviceOptions.CustomName = options.CustomName; + deviceOptions.CustomName = deviceName; await dbContext.SaveChangesAsync().ConfigureAwait(false); - DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs>(new Tuple(deviceId, options))); + DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs>(new Tuple(deviceId, deviceOptions))); } /// diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index 6ff4422d21..7e696c3b38 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -70,7 +70,7 @@ namespace MediaBrowser.Controller.Devices /// bool CanAccessDevice(User user, string deviceId); - Task UpdateDeviceOptions(string deviceId, DeviceOptions options); + Task UpdateDeviceOptions(string deviceId, string deviceName); Task GetDeviceOptions(string deviceId); } From 15baf04bd2bfc2850c4f516253f1925b40a02f5e Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 13 Aug 2021 20:35:31 -0400 Subject: [PATCH 46/82] Add IAuditableEntity --- Jellyfin.Data/Entities/Security/Device.cs | 11 +- Jellyfin.Data/Interfaces/IAuditableEntity.cs | 20 + .../Migrations/20210602224232_AddDevices.cs | 128 ------- ... => 20210814002109_AddDevices.Designer.cs} | 10 +- .../Migrations/20210814002109_AddDevices.cs | 348 ++++++++++++++++++ .../Migrations/JellyfinDbModelSnapshot.cs | 8 +- 6 files changed, 388 insertions(+), 137 deletions(-) create mode 100644 Jellyfin.Data/Interfaces/IAuditableEntity.cs delete mode 100644 Jellyfin.Server.Implementations/Migrations/20210602224232_AddDevices.cs rename Jellyfin.Server.Implementations/Migrations/{20210602224232_AddDevices.Designer.cs => 20210814002109_AddDevices.Designer.cs} (98%) create mode 100644 Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.cs diff --git a/Jellyfin.Data/Entities/Security/Device.cs b/Jellyfin.Data/Entities/Security/Device.cs index 3d0269229d..9490323b19 100644 --- a/Jellyfin.Data/Entities/Security/Device.cs +++ b/Jellyfin.Data/Entities/Security/Device.cs @@ -2,13 +2,14 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Globalization; +using Jellyfin.Data.Interfaces; namespace Jellyfin.Data.Entities.Security { /// /// An entity representing a device. /// - public class Device + public class Device : IAuditableEntity { /// /// Initializes a new instance of the class. @@ -28,6 +29,7 @@ namespace Jellyfin.Data.Entities.Security AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); DateCreated = DateTime.UtcNow; + DateModified = DateCreated; DateLastActivity = DateCreated; // Non-nullable for EF Core, as this is a required relationship. @@ -83,11 +85,12 @@ namespace Jellyfin.Data.Entities.Security /// public bool IsActive { get; set; } - /// - /// Gets or sets the date this device was created. - /// + /// public DateTime DateCreated { get; set; } + /// + public DateTime DateModified { get; set; } + /// /// Gets or sets the date of last activity. /// diff --git a/Jellyfin.Data/Interfaces/IAuditableEntity.cs b/Jellyfin.Data/Interfaces/IAuditableEntity.cs new file mode 100644 index 0000000000..4420446ae6 --- /dev/null +++ b/Jellyfin.Data/Interfaces/IAuditableEntity.cs @@ -0,0 +1,20 @@ +using System; + +namespace Jellyfin.Data.Interfaces +{ + /// + /// An interface representing an entity that has creation/modification dates. + /// + public interface IAuditableEntity + { + /// + /// Gets the date this entity was created. + /// + public DateTime DateCreated { get; } + + /// + /// Gets or sets the date this entity was modified. + /// + public DateTime DateModified { get; set; } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20210602224232_AddDevices.cs b/Jellyfin.Server.Implementations/Migrations/20210602224232_AddDevices.cs deleted file mode 100644 index 110ca1cc39..0000000000 --- a/Jellyfin.Server.Implementations/Migrations/20210602224232_AddDevices.cs +++ /dev/null @@ -1,128 +0,0 @@ -#pragma warning disable CS1591 -#pragma warning disable SA1601 - -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Jellyfin.Server.Implementations.Migrations -{ - public partial class AddDevices : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "ApiKeys", - schema: "jellyfin", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - DateCreated = table.Column(type: "TEXT", nullable: false), - DateLastActivity = table.Column(type: "TEXT", nullable: false), - Name = table.Column(type: "TEXT", maxLength: 64, nullable: false), - AccessToken = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ApiKeys", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "DeviceOptions", - schema: "jellyfin", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - DeviceId = table.Column(type: "TEXT", nullable: false), - CustomName = table.Column(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_DeviceOptions", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Devices", - schema: "jellyfin", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - UserId = table.Column(type: "TEXT", nullable: false), - AccessToken = table.Column(type: "TEXT", nullable: false), - AppName = table.Column(type: "TEXT", maxLength: 64, nullable: false), - AppVersion = table.Column(type: "TEXT", maxLength: 32, nullable: false), - DeviceName = table.Column(type: "TEXT", maxLength: 64, nullable: false), - DeviceId = table.Column(type: "TEXT", maxLength: 256, nullable: false), - IsActive = table.Column(type: "INTEGER", nullable: false), - DateCreated = table.Column(type: "TEXT", nullable: false), - DateLastActivity = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Devices", x => x.Id); - table.ForeignKey( - name: "FK_Devices_Users_UserId", - column: x => x.UserId, - principalSchema: "jellyfin", - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_ApiKeys_AccessToken", - schema: "jellyfin", - table: "ApiKeys", - column: "AccessToken", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_DeviceOptions_DeviceId", - schema: "jellyfin", - table: "DeviceOptions", - column: "DeviceId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Devices_AccessToken_DateLastActivity", - schema: "jellyfin", - table: "Devices", - columns: new[] { "AccessToken", "DateLastActivity" }); - - migrationBuilder.CreateIndex( - name: "IX_Devices_DeviceId", - schema: "jellyfin", - table: "Devices", - column: "DeviceId"); - - migrationBuilder.CreateIndex( - name: "IX_Devices_DeviceId_DateLastActivity", - schema: "jellyfin", - table: "Devices", - columns: new[] { "DeviceId", "DateLastActivity" }); - - migrationBuilder.CreateIndex( - name: "IX_Devices_UserId_DeviceId", - schema: "jellyfin", - table: "Devices", - columns: new[] { "UserId", "DeviceId" }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "ApiKeys", - schema: "jellyfin"); - - migrationBuilder.DropTable( - name: "DeviceOptions", - schema: "jellyfin"); - - migrationBuilder.DropTable( - name: "Devices", - schema: "jellyfin"); - } - } -} diff --git a/Jellyfin.Server.Implementations/Migrations/20210602224232_AddDevices.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs similarity index 98% rename from Jellyfin.Server.Implementations/Migrations/20210602224232_AddDevices.Designer.cs rename to Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs index 933e82229d..7e9566e2ea 100644 --- a/Jellyfin.Server.Implementations/Migrations/20210602224232_AddDevices.Designer.cs +++ b/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs @@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Jellyfin.Server.Implementations.Migrations { [DbContext(typeof(JellyfinDb))] - [Migration("20210602224232_AddDevices")] + [Migration("20210814002109_AddDevices")] partial class AddDevices { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -19,7 +19,7 @@ namespace Jellyfin.Server.Implementations.Migrations #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("jellyfin") - .HasAnnotation("ProductVersion", "5.0.6"); + .HasAnnotation("ProductVersion", "5.0.7"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -342,7 +342,8 @@ namespace Jellyfin.Server.Implementations.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("AccessToken") + b.Property("AccessToken") + .IsRequired() .HasColumnType("TEXT"); b.Property("DateCreated") @@ -390,6 +391,9 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property("DateLastActivity") .HasColumnType("TEXT"); + b.Property("DateModified") + .HasColumnType("TEXT"); + b.Property("DeviceId") .IsRequired() .HasMaxLength(256) diff --git a/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.cs b/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.cs new file mode 100644 index 0000000000..81bf890075 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.cs @@ -0,0 +1,348 @@ +#pragma warning disable CS1591, SA1601 + +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Jellyfin.Server.Implementations.Migrations +{ + public partial class AddDevices : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_ImageInfos_Users_UserId", + schema: "jellyfin", + table: "ImageInfos"); + + migrationBuilder.DropForeignKey( + name: "FK_Permissions_Users_Permission_Permissions_Guid", + schema: "jellyfin", + table: "Permissions"); + + migrationBuilder.DropForeignKey( + name: "FK_Preferences_Users_Preference_Preferences_Guid", + schema: "jellyfin", + table: "Preferences"); + + migrationBuilder.DropIndex( + name: "IX_Preferences_Preference_Preferences_Guid", + schema: "jellyfin", + table: "Preferences"); + + migrationBuilder.DropIndex( + name: "IX_Permissions_Permission_Permissions_Guid", + schema: "jellyfin", + table: "Permissions"); + + migrationBuilder.DropIndex( + name: "IX_DisplayPreferences_UserId", + schema: "jellyfin", + table: "DisplayPreferences"); + + migrationBuilder.DropIndex( + name: "IX_CustomItemDisplayPreferences_UserId", + schema: "jellyfin", + table: "CustomItemDisplayPreferences"); + + migrationBuilder.AlterColumn( + name: "Username", + schema: "jellyfin", + table: "Users", + type: "TEXT", + maxLength: 255, + nullable: false, + collation: "NOCASE", + oldClrType: typeof(string), + oldType: "TEXT", + oldMaxLength: 255); + + migrationBuilder.AddColumn( + name: "UserId", + schema: "jellyfin", + table: "Preferences", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "UserId", + schema: "jellyfin", + table: "Permissions", + type: "TEXT", + nullable: true); + + migrationBuilder.CreateTable( + name: "ApiKeys", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DateCreated = table.Column(type: "TEXT", nullable: false), + DateLastActivity = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 64, nullable: false), + AccessToken = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiKeys", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "DeviceOptions", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DeviceId = table.Column(type: "TEXT", nullable: false), + CustomName = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_DeviceOptions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Devices", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(type: "TEXT", nullable: false), + AccessToken = table.Column(type: "TEXT", nullable: false), + AppName = table.Column(type: "TEXT", maxLength: 64, nullable: false), + AppVersion = table.Column(type: "TEXT", maxLength: 32, nullable: false), + DeviceName = table.Column(type: "TEXT", maxLength: 64, nullable: false), + DeviceId = table.Column(type: "TEXT", maxLength: 256, nullable: false), + IsActive = table.Column(type: "INTEGER", nullable: false), + DateCreated = table.Column(type: "TEXT", nullable: false), + DateModified = table.Column(type: "TEXT", nullable: false), + DateLastActivity = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Devices", x => x.Id); + table.ForeignKey( + name: "FK_Devices_Users_UserId", + column: x => x.UserId, + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Users_Username", + schema: "jellyfin", + table: "Users", + column: "Username", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Preferences_UserId_Kind", + schema: "jellyfin", + table: "Preferences", + columns: new[] { "UserId", "Kind" }, + unique: true, + filter: "[UserId] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_Permissions_UserId_Kind", + schema: "jellyfin", + table: "Permissions", + columns: new[] { "UserId", "Kind" }, + unique: true, + filter: "[UserId] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeys_AccessToken", + schema: "jellyfin", + table: "ApiKeys", + column: "AccessToken", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_DeviceOptions_DeviceId", + schema: "jellyfin", + table: "DeviceOptions", + column: "DeviceId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Devices_AccessToken_DateLastActivity", + schema: "jellyfin", + table: "Devices", + columns: new[] { "AccessToken", "DateLastActivity" }); + + migrationBuilder.CreateIndex( + name: "IX_Devices_DeviceId", + schema: "jellyfin", + table: "Devices", + column: "DeviceId"); + + migrationBuilder.CreateIndex( + name: "IX_Devices_DeviceId_DateLastActivity", + schema: "jellyfin", + table: "Devices", + columns: new[] { "DeviceId", "DateLastActivity" }); + + migrationBuilder.CreateIndex( + name: "IX_Devices_UserId_DeviceId", + schema: "jellyfin", + table: "Devices", + columns: new[] { "UserId", "DeviceId" }); + + migrationBuilder.AddForeignKey( + name: "FK_ImageInfos_Users_UserId", + schema: "jellyfin", + table: "ImageInfos", + column: "UserId", + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Permissions_Users_UserId", + schema: "jellyfin", + table: "Permissions", + column: "UserId", + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Preferences_Users_UserId", + schema: "jellyfin", + table: "Preferences", + column: "UserId", + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_ImageInfos_Users_UserId", + schema: "jellyfin", + table: "ImageInfos"); + + migrationBuilder.DropForeignKey( + name: "FK_Permissions_Users_UserId", + schema: "jellyfin", + table: "Permissions"); + + migrationBuilder.DropForeignKey( + name: "FK_Preferences_Users_UserId", + schema: "jellyfin", + table: "Preferences"); + + migrationBuilder.DropTable( + name: "ApiKeys", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "DeviceOptions", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "Devices", + schema: "jellyfin"); + + migrationBuilder.DropIndex( + name: "IX_Users_Username", + schema: "jellyfin", + table: "Users"); + + migrationBuilder.DropIndex( + name: "IX_Preferences_UserId_Kind", + schema: "jellyfin", + table: "Preferences"); + + migrationBuilder.DropIndex( + name: "IX_Permissions_UserId_Kind", + schema: "jellyfin", + table: "Permissions"); + + migrationBuilder.DropColumn( + name: "UserId", + schema: "jellyfin", + table: "Preferences"); + + migrationBuilder.DropColumn( + name: "UserId", + schema: "jellyfin", + table: "Permissions"); + + migrationBuilder.AlterColumn( + name: "Username", + schema: "jellyfin", + table: "Users", + type: "TEXT", + maxLength: 255, + nullable: false, + oldClrType: typeof(string), + oldType: "TEXT", + oldMaxLength: 255, + oldCollation: "NOCASE"); + + migrationBuilder.CreateIndex( + name: "IX_Preferences_Preference_Preferences_Guid", + schema: "jellyfin", + table: "Preferences", + column: "Preference_Preferences_Guid"); + + migrationBuilder.CreateIndex( + name: "IX_Permissions_Permission_Permissions_Guid", + schema: "jellyfin", + table: "Permissions", + column: "Permission_Permissions_Guid"); + + migrationBuilder.CreateIndex( + name: "IX_DisplayPreferences_UserId", + schema: "jellyfin", + table: "DisplayPreferences", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_CustomItemDisplayPreferences_UserId", + schema: "jellyfin", + table: "CustomItemDisplayPreferences", + column: "UserId"); + + migrationBuilder.AddForeignKey( + name: "FK_ImageInfos_Users_UserId", + schema: "jellyfin", + table: "ImageInfos", + column: "UserId", + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_Permissions_Users_Permission_Permissions_Guid", + schema: "jellyfin", + table: "Permissions", + column: "Permission_Permissions_Guid", + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_Preferences_Users_Preference_Preferences_Guid", + schema: "jellyfin", + table: "Preferences", + column: "Preference_Preferences_Guid", + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index b508b834ce..fcc360e260 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", "5.0.6"); + .HasAnnotation("ProductVersion", "5.0.7"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -338,7 +338,8 @@ namespace Jellyfin.Server.Implementations.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("AccessToken") + b.Property("AccessToken") + .IsRequired() .HasColumnType("TEXT"); b.Property("DateCreated") @@ -386,6 +387,9 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property("DateLastActivity") .HasColumnType("TEXT"); + b.Property("DateModified") + .HasColumnType("TEXT"); + b.Property("DeviceId") .IsRequired() .HasMaxLength(256) From 1b197a2c2abf306330ffa08d3a61812c1fa0432e Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 13 Aug 2021 21:08:38 -0400 Subject: [PATCH 47/82] Fix QuickConnect tests, move class to proper namespace --- .../QuickConnect/QuickConnectManagerTests.cs | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/tests/Jellyfin.Server.Implementations.Tests/QuickConnect/QuickConnectManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/QuickConnect/QuickConnectManagerTests.cs index 365acfa341..043363ae3d 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/QuickConnect/QuickConnectManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/QuickConnect/QuickConnectManagerTests.cs @@ -1,17 +1,28 @@ using System; +using System.Linq; +using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; using Emby.Server.Implementations.QuickConnect; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; using MediaBrowser.Model.Configuration; using Moq; using Xunit; -namespace Jellyfin.Server.Implementations.Tests.LiveTv +namespace Jellyfin.Server.Implementations.Tests.QuickConnect { public class QuickConnectManagerTests { + private static readonly AuthorizationInfo _quickConnectAuthInfo = new AuthorizationInfo + { + Device = "Device", + DeviceId = "DeviceId", + Client = "Client", + Version = "1.0.0" + }; + private readonly Fixture _fixture; private readonly ServerConfiguration _config; private readonly QuickConnectManager _quickConnectManager; @@ -27,6 +38,12 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv { ConfigureMembers = true }).Inject(configManager.Object); + + // User object contains circular references. + _fixture.Behaviors.OfType().ToList() + .ForEach(b => _fixture.Behaviors.Remove(b)); + _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + _quickConnectManager = _fixture.Create(); } @@ -36,7 +53,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv [Fact] public void TryConnect_QuickConnectUnavailable_ThrowsAuthenticationException() - => Assert.Throws(_quickConnectManager.TryConnect); + => Assert.Throws(() => _quickConnectManager.TryConnect(_quickConnectAuthInfo)); [Fact] public void CheckRequestStatus_QuickConnectUnavailable_ThrowsAuthenticationException() @@ -44,7 +61,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv [Fact] public void AuthorizeRequest_QuickConnectUnavailable_ThrowsAuthenticationException() - => Assert.Throws(() => _quickConnectManager.AuthorizeRequest(Guid.Empty, string.Empty)); + => Assert.ThrowsAsync(() => _quickConnectManager.AuthorizeRequest(Guid.Empty, string.Empty)); [Fact] public void IsEnabled_QuickConnectAvailable_True() @@ -57,17 +74,17 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv public void CheckRequestStatus_QuickConnectAvailable_Success() { _config.QuickConnectAvailable = true; - var res1 = _quickConnectManager.TryConnect(); + var res1 = _quickConnectManager.TryConnect(_quickConnectAuthInfo); var res2 = _quickConnectManager.CheckRequestStatus(res1.Secret); Assert.Equal(res1, res2); } [Fact] - public void AuthorizeRequest_QuickConnectAvailable_Success() + public async Task AuthorizeRequest_QuickConnectAvailable_Success() { _config.QuickConnectAvailable = true; - var res = _quickConnectManager.TryConnect(); - Assert.True(_quickConnectManager.AuthorizeRequest(Guid.Empty, res.Code)); + var res = _quickConnectManager.TryConnect(_quickConnectAuthInfo); + Assert.True(await _quickConnectManager.AuthorizeRequest(Guid.Empty, res.Code)); } } } From 1615663bd240fadd4a2d03c44256880dd40c7cb2 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 13 Aug 2021 21:08:49 -0400 Subject: [PATCH 48/82] Remove old response code documentation --- Jellyfin.Api/Controllers/DevicesController.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index ebe7b7584b..8292cf83b5 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -99,11 +99,9 @@ namespace Jellyfin.Api.Controllers /// Device Id. /// Device Options. /// Device options updated. - /// Device not found. - /// A on success, or a if the device could not be found. + /// A . [HttpPost("Options")] [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task UpdateDeviceOptions( [FromQuery, Required] string id, [FromBody, Required] DeviceOptionsDto deviceOptions) From 8a1e55251e23681e19bef318540f8be5d750be2d Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 13 Aug 2021 21:25:19 -0400 Subject: [PATCH 49/82] Fix devices migration --- .../Migrations/20210814002109_AddDevices.cs | 220 ------------------ 1 file changed, 220 deletions(-) diff --git a/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.cs b/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.cs index 81bf890075..ac062317a7 100644 --- a/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.cs +++ b/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.cs @@ -9,67 +9,6 @@ namespace Jellyfin.Server.Implementations.Migrations { protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.DropForeignKey( - name: "FK_ImageInfos_Users_UserId", - schema: "jellyfin", - table: "ImageInfos"); - - migrationBuilder.DropForeignKey( - name: "FK_Permissions_Users_Permission_Permissions_Guid", - schema: "jellyfin", - table: "Permissions"); - - migrationBuilder.DropForeignKey( - name: "FK_Preferences_Users_Preference_Preferences_Guid", - schema: "jellyfin", - table: "Preferences"); - - migrationBuilder.DropIndex( - name: "IX_Preferences_Preference_Preferences_Guid", - schema: "jellyfin", - table: "Preferences"); - - migrationBuilder.DropIndex( - name: "IX_Permissions_Permission_Permissions_Guid", - schema: "jellyfin", - table: "Permissions"); - - migrationBuilder.DropIndex( - name: "IX_DisplayPreferences_UserId", - schema: "jellyfin", - table: "DisplayPreferences"); - - migrationBuilder.DropIndex( - name: "IX_CustomItemDisplayPreferences_UserId", - schema: "jellyfin", - table: "CustomItemDisplayPreferences"); - - migrationBuilder.AlterColumn( - name: "Username", - schema: "jellyfin", - table: "Users", - type: "TEXT", - maxLength: 255, - nullable: false, - collation: "NOCASE", - oldClrType: typeof(string), - oldType: "TEXT", - oldMaxLength: 255); - - migrationBuilder.AddColumn( - name: "UserId", - schema: "jellyfin", - table: "Preferences", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "UserId", - schema: "jellyfin", - table: "Permissions", - type: "TEXT", - nullable: true); - migrationBuilder.CreateTable( name: "ApiKeys", schema: "jellyfin", @@ -132,29 +71,6 @@ namespace Jellyfin.Server.Implementations.Migrations onDelete: ReferentialAction.Cascade); }); - migrationBuilder.CreateIndex( - name: "IX_Users_Username", - schema: "jellyfin", - table: "Users", - column: "Username", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Preferences_UserId_Kind", - schema: "jellyfin", - table: "Preferences", - columns: new[] { "UserId", "Kind" }, - unique: true, - filter: "[UserId] IS NOT NULL"); - - migrationBuilder.CreateIndex( - name: "IX_Permissions_UserId_Kind", - schema: "jellyfin", - table: "Permissions", - columns: new[] { "UserId", "Kind" }, - unique: true, - filter: "[UserId] IS NOT NULL"); - migrationBuilder.CreateIndex( name: "IX_ApiKeys_AccessToken", schema: "jellyfin", @@ -192,55 +108,10 @@ namespace Jellyfin.Server.Implementations.Migrations schema: "jellyfin", table: "Devices", columns: new[] { "UserId", "DeviceId" }); - - migrationBuilder.AddForeignKey( - name: "FK_ImageInfos_Users_UserId", - schema: "jellyfin", - table: "ImageInfos", - column: "UserId", - principalSchema: "jellyfin", - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - - migrationBuilder.AddForeignKey( - name: "FK_Permissions_Users_UserId", - schema: "jellyfin", - table: "Permissions", - column: "UserId", - principalSchema: "jellyfin", - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - - migrationBuilder.AddForeignKey( - name: "FK_Preferences_Users_UserId", - schema: "jellyfin", - table: "Preferences", - column: "UserId", - principalSchema: "jellyfin", - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); } protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropForeignKey( - name: "FK_ImageInfos_Users_UserId", - schema: "jellyfin", - table: "ImageInfos"); - - migrationBuilder.DropForeignKey( - name: "FK_Permissions_Users_UserId", - schema: "jellyfin", - table: "Permissions"); - - migrationBuilder.DropForeignKey( - name: "FK_Preferences_Users_UserId", - schema: "jellyfin", - table: "Preferences"); - migrationBuilder.DropTable( name: "ApiKeys", schema: "jellyfin"); @@ -252,97 +123,6 @@ namespace Jellyfin.Server.Implementations.Migrations migrationBuilder.DropTable( name: "Devices", schema: "jellyfin"); - - migrationBuilder.DropIndex( - name: "IX_Users_Username", - schema: "jellyfin", - table: "Users"); - - migrationBuilder.DropIndex( - name: "IX_Preferences_UserId_Kind", - schema: "jellyfin", - table: "Preferences"); - - migrationBuilder.DropIndex( - name: "IX_Permissions_UserId_Kind", - schema: "jellyfin", - table: "Permissions"); - - migrationBuilder.DropColumn( - name: "UserId", - schema: "jellyfin", - table: "Preferences"); - - migrationBuilder.DropColumn( - name: "UserId", - schema: "jellyfin", - table: "Permissions"); - - migrationBuilder.AlterColumn( - name: "Username", - schema: "jellyfin", - table: "Users", - type: "TEXT", - maxLength: 255, - nullable: false, - oldClrType: typeof(string), - oldType: "TEXT", - oldMaxLength: 255, - oldCollation: "NOCASE"); - - migrationBuilder.CreateIndex( - name: "IX_Preferences_Preference_Preferences_Guid", - schema: "jellyfin", - table: "Preferences", - column: "Preference_Preferences_Guid"); - - migrationBuilder.CreateIndex( - name: "IX_Permissions_Permission_Permissions_Guid", - schema: "jellyfin", - table: "Permissions", - column: "Permission_Permissions_Guid"); - - migrationBuilder.CreateIndex( - name: "IX_DisplayPreferences_UserId", - schema: "jellyfin", - table: "DisplayPreferences", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_CustomItemDisplayPreferences_UserId", - schema: "jellyfin", - table: "CustomItemDisplayPreferences", - column: "UserId"); - - migrationBuilder.AddForeignKey( - name: "FK_ImageInfos_Users_UserId", - schema: "jellyfin", - table: "ImageInfos", - column: "UserId", - principalSchema: "jellyfin", - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - - migrationBuilder.AddForeignKey( - name: "FK_Permissions_Users_Permission_Permissions_Guid", - schema: "jellyfin", - table: "Permissions", - column: "Permission_Permissions_Guid", - principalSchema: "jellyfin", - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - - migrationBuilder.AddForeignKey( - name: "FK_Preferences_Users_Preference_Preferences_Guid", - schema: "jellyfin", - table: "Preferences", - column: "Preference_Preferences_Guid", - principalSchema: "jellyfin", - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); } } } From 2345646ff159f474588e24c29ab94a97a4f41e6d Mon Sep 17 00:00:00 2001 From: boolemancer Date: Sat, 14 Aug 2021 02:55:51 -0700 Subject: [PATCH 50/82] Fix explicit stream selection in MediaEncoder.ExtractImageInternal --- .../Encoder/MediaEncoder.cs | 12 ++---------- .../MediaInfo/VideoImageProvider.cs | 18 +----------------- 2 files changed, 3 insertions(+), 27 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 412a953216..e0a8102f92 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -503,15 +503,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { var inputArgument = GetInputArgument(inputFile, mediaSource); - if (isAudio) - { - if (imageStreamIndex.HasValue && imageStreamIndex.Value > 0) - { - // It seems for audio files we need to subtract 1 (for the audio stream??) - imageStreamIndex = imageStreamIndex.Value - 1; - } - } - else + if (!isAudio) { // The failure of HDR extraction usually occurs when using custom ffmpeg that does not contain the zscale filter. try @@ -582,7 +574,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _ => string.Empty }; - var mapArg = imageStreamIndex.HasValue ? (" -map 0:v:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty; + var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty; var enableHdrExtraction = allowTonemap && string.Equals(videoStream?.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase); if (enableHdrExtraction) diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs index 30af6710ab..453938be79 100644 --- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs @@ -88,22 +88,6 @@ namespace MediaBrowser.Providers.MediaInfo if (imageStream != null) { - // Instead of using the raw stream index, we need to use nth video/embedded image stream - var videoIndex = -1; - foreach (var mediaStream in mediaStreams) - { - if (mediaStream.Type == MediaStreamType.Video || - mediaStream.Type == MediaStreamType.EmbeddedImage) - { - videoIndex++; - } - - if (mediaStream == imageStream) - { - break; - } - } - MediaSourceInfo mediaSource = new MediaSourceInfo { VideoType = item.VideoType, @@ -111,7 +95,7 @@ namespace MediaBrowser.Providers.MediaInfo Protocol = item.PathProtocol.Value, }; - extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, mediaSource, imageStream, videoIndex, cancellationToken).ConfigureAwait(false); + extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, mediaSource, imageStream, imageStream.Index, cancellationToken).ConfigureAwait(false); } else { From 3300d2d7d3086b84e0158c8e507b2412d54ebe04 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Tue, 27 Jul 2021 14:02:49 +0200 Subject: [PATCH 51/82] Enable people for audio files --- MediaBrowser.Controller/Entities/Audio/Audio.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index 7bf1219ec2..536668e508 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -43,7 +43,7 @@ namespace MediaBrowser.Controller.Entities.Audio public override bool SupportsPlayedStatus => true; [JsonIgnore] - public override bool SupportsPeople => false; + public override bool SupportsPeople => true; [JsonIgnore] public override bool SupportsAddingToPlaylist => true; From 7e71c25059d373818f228cc524ac4d68f1ea0602 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Tue, 27 Jul 2021 14:46:18 +0200 Subject: [PATCH 52/82] Add test for audio file ffprobe normalization --- .../Probing/ProbeResultNormalizerTests.cs | 28 ++++ .../Test Data/Probing/music_metadata.json | 144 ++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_metadata.json diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index 59037c2636..690c5d1c14 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs @@ -4,6 +4,7 @@ using System.IO; using System.Text.Json; using Jellyfin.Extensions.Json; using MediaBrowser.MediaEncoding.Probing; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging.Abstractions; @@ -91,5 +92,32 @@ namespace Jellyfin.MediaEncoding.Tests.Probing Assert.Contains("Pop", res.Genres); Assert.Contains("Jazz", res.Genres); } + + [Fact] + public void GetMediaInfo_Music_Success() + { + var bytes = File.ReadAllBytes("Test Data/Probing/music_metadata.json"); + var internalMediaInfoResult = JsonSerializer.Deserialize(bytes, _jsonOptions); + MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, null, true, "Test Data/Probing/music.flac", MediaProtocol.File); + + Assert.Equal("UP NO MORE", res.Name); + Assert.Single(res.Artists); + Assert.Equal("TWICE", res.Artists[0]); + Assert.Equal("Eyes wide open", res.Album); + Assert.Equal(2020, res.ProductionYear); + Assert.True(res.PremiereDate.HasValue); + Assert.Equal(DateTime.Parse("2020-10-26T00:00Z", DateTimeFormatInfo.CurrentInfo).ToUniversalTime(), res.PremiereDate); + Assert.NotEmpty(res.People); + Assert.Equal("Krysta Youngs", res.People[0].Name); + Assert.Equal(PersonType.Composer, res.People[0].Type); + Assert.Equal("Julia Ross", res.People[1].Name); + Assert.Equal(PersonType.Composer, res.People[1].Type); + Assert.Equal("Yiwoomin", res.People[2].Name); + Assert.Equal(PersonType.Composer, res.People[2].Type); + Assert.Equal("Ji-hyo Park", res.People[3].Name); + Assert.Equal(PersonType.Lyricist, res.People[3].Type); + Assert.NotEmpty(res.Genres); + Assert.Equal(new string[] { "Electronic", "Trance", "Dance", "Jazz" }, res.Genres); + } } } diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_metadata.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_metadata.json new file mode 100644 index 0000000000..6530629fe8 --- /dev/null +++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_metadata.json @@ -0,0 +1,144 @@ +{ + "streams": [ + { + "index": 0, + "codec_name": "flac", + "codec_long_name": "FLAC (Free Lossless Audio Codec)", + "codec_type": "audio", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "sample_fmt": "s16", + "sample_rate": "44100", + "channels": 2, + "channel_layout": "stereo", + "bits_per_sample": 0, + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/44100", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 9447984, + "duration": "214.240000", + "bits_per_raw_sample": "16", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + } + }, + { + "index": 1, + "codec_name": "mjpeg", + "codec_long_name": "Motion JPEG", + "profile": "Baseline", + "codec_type": "video", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "width": 500, + "height": 500, + "coded_width": 500, + "coded_height": 500, + "closed_captions": 0, + "has_b_frames": 0, + "sample_aspect_ratio": "1:1", + "display_aspect_ratio": "1:1", + "pix_fmt": "yuvj420p", + "level": -99, + "color_range": "pc", + "color_space": "bt470bg", + "chroma_location": "center", + "refs": 1, + "r_frame_rate": "90000/1", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 19281600, + "duration": "214.240000", + "bits_per_raw_sample": "8", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 1, + "timed_thumbnails": 0 + }, + "tags": { + "comment": "Cover (front)" + } + } + ], + "format": { + "filename": "03 UP NO MORE.flac", + "nb_streams": 2, + "nb_programs": 0, + "format_name": "flac", + "format_long_name": "raw FLAC", + "start_time": "0.000000", + "duration": "214.240000", + "size": "28714641", + "bit_rate": "1072242", + "probe_score": 100, + "tags": { + "MUSICBRAINZ_RELEASEGROUPID": "aa05ff10-8589-4c9c-a0d4-6b024f4e4556", + "ORIGINALDATE": "2020-10-26", + "ORIGINALYEAR": "2020", + "RELEASETYPE": "album", + "MUSICBRAINZ_ALBUMID": "222e6610-75c9-400e-8dc3-bb61f9fc5ca7", + "SCRIPT": "Latn", + "ALBUM": "Eyes wide open", + "RELEASECOUNTRY": "JP", + "BARCODE": "190295105280", + "LABEL": "JYP Entertainment", + "RELEASESTATUS": "official", + "DATE": "2020-10-26", + "MUSICBRAINZ_ALBUMARTISTID": "8da127cc-c432-418f-b356-ef36210d82ac", + "album_artist": "TWICE", + "ALBUMARTISTSORT": "TWICE", + "TOTALDISCS": "1", + "TOTALTRACKS": "13", + "MEDIA": "Digital Media", + "disc": "1", + "MUSICBRAINZ_TRACKID": "7d1a1044-b564-480d-9df3-22f9656fdb97", + "TITLE": "UP NO MORE", + "ISRC": "US5TA2000136", + "PERFORMER": "Yiwoomin (electric piano);Yiwoomin (synthesizer);Yiwoomin (bass);Yiwoomin (guitar);TWICE;Tzu-yu Chou (vocals);Momo Hirai (vocals);Na-yeon Im (vocals);Da-hyun Kim (vocals);Sana Minatozaki (vocals);Mina Myoui (vocals);Ji-hyo Park (vocals);Chae-young Son (vocals);Jeong-yeon Yoo (vocals);Perrie (background vocals)", + "MIXER": "Bong Won Shin", + "ARRANGER": "Krysta Youngs;Julia Ross;Yiwoomin", + "MUSICBRAINZ_WORKID": "02b37083-0337-4721-9f17-bf31971043e8", + "LANGUAGE": "kor;eng", + "WORK": "Up No More", + "COMPOSER": "Krysta Youngs;Julia Ross;Yiwoomin", + "COMPOSERSORT": "Krysta Youngs;Ross, Julia;Yiwoomin", + "LYRICIST": "Ji-hyo Park", + "MUSICBRAINZ_ARTISTID": "8da127cc-c432-418f-b356-ef36210d82ac", + "ARTIST": "TWICE", + "ARTISTSORT": "TWICE", + "ARTISTS": "TWICE", + "MUSICBRAINZ_RELEASETRACKID": "ad49b840-da9e-4e7c-924b-29fdee187052", + "track": "3", + "GENRE": "Electronic;Trance;Dance;Jazz", + "WEBSITE": "http://twice.jype.com/;http://www.twicejapan.com/", + "ACOUSTID_ID": "aae2e972-108c-4d0c-8e31-9d078283e3dc", + "MOOD": "Not acoustic;Not aggressive;Electronic;Happy;Party;Not relaxed;Not sad", + "TRACKTOTAL": "13", + "DISCTOTAL": "1" + } + } +} From 24083d2e38c17e005c536339d555355b9155485f Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Tue, 27 Jul 2021 16:25:46 +0200 Subject: [PATCH 53/82] Address comments --- .../Probing/ProbeResultNormalizerTests.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index 690c5d1c14..4d6b7a5140 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs @@ -107,7 +107,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing Assert.Equal(2020, res.ProductionYear); Assert.True(res.PremiereDate.HasValue); Assert.Equal(DateTime.Parse("2020-10-26T00:00Z", DateTimeFormatInfo.CurrentInfo).ToUniversalTime(), res.PremiereDate); - Assert.NotEmpty(res.People); + Assert.Equal(4, res.People.Length); Assert.Equal("Krysta Youngs", res.People[0].Name); Assert.Equal(PersonType.Composer, res.People[0].Type); Assert.Equal("Julia Ross", res.People[1].Name); @@ -116,8 +116,11 @@ namespace Jellyfin.MediaEncoding.Tests.Probing Assert.Equal(PersonType.Composer, res.People[2].Type); Assert.Equal("Ji-hyo Park", res.People[3].Name); Assert.Equal(PersonType.Lyricist, res.People[3].Type); - Assert.NotEmpty(res.Genres); - Assert.Equal(new string[] { "Electronic", "Trance", "Dance", "Jazz" }, res.Genres); + Assert.Equal(4, res.Genres.Length); + Assert.Contains("Electronic", res.Genres); + Assert.Contains("Trance", res.Genres); + Assert.Contains("Dance", res.Genres); + Assert.Contains("Jazz", res.Genres); } } } From f35a527608fb3f6efde3f76042e0e701768eb3f2 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Tue, 27 Jul 2021 17:09:23 +0200 Subject: [PATCH 54/82] Add performers to the ffprobe normalization for audio --- .../Probing/ProbeResultNormalizer.cs | 21 +++++++++++++++++++ .../Probing/ProbeResultNormalizerTests.cs | 5 ++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 875ee6f04c..20dead0771 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Xml; using Jellyfin.Extensions; using MediaBrowser.Controller.Library; @@ -1111,6 +1112,26 @@ namespace MediaBrowser.MediaEncoding.Probing } } + if (tags.TryGetValue("performer", out var performer) && !string.IsNullOrWhiteSpace(performer)) + { + foreach (var person in Split(performer, false)) + { + Regex pattern = new Regex(@"(?.*) \((?.*)\)"); + Match match = pattern.Match(person); + + // If the performer doesn't have any instrument/role associated, it won't match. In that case, chances are it's simply a band name, so we skip it. + if (match.Success) + { + people.Add(new BaseItemPerson + { + Name = match.Groups["name"].Value, + Type = PersonType.Actor, + Role = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(match.Groups["instrument"].Value) + }); + } + } + } + // Check for writer some music is tagged that way as alternative to composer/lyricist if (tags.TryGetValue("writer", out var writer) && !string.IsNullOrWhiteSpace(writer)) { diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index 4d6b7a5140..928f275d7e 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs @@ -107,7 +107,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing Assert.Equal(2020, res.ProductionYear); Assert.True(res.PremiereDate.HasValue); Assert.Equal(DateTime.Parse("2020-10-26T00:00Z", DateTimeFormatInfo.CurrentInfo).ToUniversalTime(), res.PremiereDate); - Assert.Equal(4, res.People.Length); + Assert.Equal(18, res.People.Length); Assert.Equal("Krysta Youngs", res.People[0].Name); Assert.Equal(PersonType.Composer, res.People[0].Type); Assert.Equal("Julia Ross", res.People[1].Name); @@ -116,6 +116,9 @@ namespace Jellyfin.MediaEncoding.Tests.Probing Assert.Equal(PersonType.Composer, res.People[2].Type); Assert.Equal("Ji-hyo Park", res.People[3].Name); Assert.Equal(PersonType.Lyricist, res.People[3].Type); + Assert.Equal("Yiwoomin", res.People[4].Name); + Assert.Equal(PersonType.Actor, res.People[4].Type); + Assert.Equal("Electric Piano", res.People[4].Role); Assert.Equal(4, res.Genres.Length); Assert.Contains("Electronic", res.Genres); Assert.Contains("Trance", res.Genres); From c9b1cd1d8cfa3764eff3dfe97baac10ce229d2e9 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Tue, 27 Jul 2021 23:52:05 +0200 Subject: [PATCH 55/82] Add some new music-related person types and parse from ffprobe --- .../Probing/ProbeResultNormalizer.cs | 34 ++++++++++++++++- MediaBrowser.Model/Entities/PersonType.cs | 38 ++++++++++++++----- .../Probing/ProbeResultNormalizerTests.cs | 2 +- 3 files changed, 63 insertions(+), 11 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 20dead0771..aa6674468b 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -1132,7 +1132,7 @@ namespace MediaBrowser.MediaEncoding.Probing } } - // Check for writer some music is tagged that way as alternative to composer/lyricist + // In cases where there isn't sufficient information as to which role a writer performed on a recording, tagging software uses the "writer" tag. if (tags.TryGetValue("writer", out var writer) && !string.IsNullOrWhiteSpace(writer)) { foreach (var person in Split(writer, false)) @@ -1141,6 +1141,38 @@ namespace MediaBrowser.MediaEncoding.Probing } } + if (tags.TryGetValue("arranger", out var arranger) && !string.IsNullOrWhiteSpace(arranger)) + { + foreach (var person in Split(arranger, false)) + { + people.Add(new BaseItemPerson { Name = person, Type = PersonType.Arranger }); + } + } + + if (tags.TryGetValue("engineer", out var engineer) && !string.IsNullOrWhiteSpace(engineer)) + { + foreach (var person in Split(engineer, false)) + { + people.Add(new BaseItemPerson { Name = person, Type = PersonType.Engineer }); + } + } + + if (tags.TryGetValue("mixer", out var mixer) && !string.IsNullOrWhiteSpace(mixer)) + { + foreach (var person in Split(mixer, false)) + { + people.Add(new BaseItemPerson { Name = person, Type = PersonType.Mixer }); + } + } + + if (tags.TryGetValue("remixer", out var remixer) && !string.IsNullOrWhiteSpace(remixer)) + { + foreach (var person in Split(remixer, false)) + { + people.Add(new BaseItemPerson { Name = person, Type = PersonType.Remixer }); + } + } + audio.People = people.ToArray(); // Set album artist diff --git a/MediaBrowser.Model/Entities/PersonType.cs b/MediaBrowser.Model/Entities/PersonType.cs index 81db9c6131..7b780f7d7b 100644 --- a/MediaBrowser.Model/Entities/PersonType.cs +++ b/MediaBrowser.Model/Entities/PersonType.cs @@ -1,42 +1,42 @@ namespace MediaBrowser.Model.Entities { /// - /// Struct PersonType. + /// Types of persons. /// - public class PersonType + public static class PersonType { /// - /// The actor. + /// A person whose profession is acting on the stage, in films, or on television. /// public const string Actor = "Actor"; /// - /// The director. + /// A person who supervises the actors and other staff in a film, play, or similar production. /// public const string Director = "Director"; /// - /// The composer. + /// A person who writes music, especially as a professional occupation. /// public const string Composer = "Composer"; /// - /// The writer. + /// A writer of a book, article, or document. Can also be used as a generic term for music writer if there is a lack of specificity. /// public const string Writer = "Writer"; /// - /// The guest star. + /// A well-known actor or other performer who appears in a work in which they do not have a regular role. /// public const string GuestStar = "GuestStar"; /// - /// The producer. + /// A person responsible for the financial and managerial aspects of the making of a film or broadcast or for staging a play, opera, etc. /// public const string Producer = "Producer"; /// - /// The conductor. + /// A person who directs the performance of an orchestra or choir. /// public const string Conductor = "Conductor"; @@ -44,5 +44,25 @@ namespace MediaBrowser.Model.Entities /// The lyricist. /// public const string Lyricist = "Lyricist"; + + /// + /// A person who writes the words to a song or musical. + /// + public const string Arranger = "Arranger"; + + /// + /// An audio engineer who performed a general engineering role. + /// + public const string Engineer = "Engineer"; + + /// + /// An engineer responsible for using a mixing console to mix a recorded track into a single piece of music suitable for release. + /// + public const string Mixer = "Mixer"; + + /// + /// A person who remixed a recording by taking one or more other tracks, substantially altering them and mixing them together with other material. + /// + public const string Remixer = "Remixer"; } } diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index 928f275d7e..fcb85a3acf 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs @@ -107,7 +107,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing Assert.Equal(2020, res.ProductionYear); Assert.True(res.PremiereDate.HasValue); Assert.Equal(DateTime.Parse("2020-10-26T00:00Z", DateTimeFormatInfo.CurrentInfo).ToUniversalTime(), res.PremiereDate); - Assert.Equal(18, res.People.Length); + Assert.Equal(22, res.People.Length); Assert.Equal("Krysta Youngs", res.People[0].Name); Assert.Equal(PersonType.Composer, res.People[0].Type); Assert.Equal("Julia Ross", res.People[1].Name); From 8594ee7a2275403d0569529cd1e1237e10cc7d77 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Wed, 28 Jul 2021 23:25:45 +0200 Subject: [PATCH 56/82] Fix documentation for lyricist and arranger --- MediaBrowser.Model/Entities/PersonType.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Model/Entities/PersonType.cs b/MediaBrowser.Model/Entities/PersonType.cs index 7b780f7d7b..b985507f0c 100644 --- a/MediaBrowser.Model/Entities/PersonType.cs +++ b/MediaBrowser.Model/Entities/PersonType.cs @@ -41,12 +41,12 @@ namespace MediaBrowser.Model.Entities public const string Conductor = "Conductor"; /// - /// The lyricist. + /// A person who writes the words to a song or musical. /// public const string Lyricist = "Lyricist"; /// - /// A person who writes the words to a song or musical. + /// A person who adapts a musical composition for performance. /// public const string Arranger = "Arranger"; From d82c2e423766566883be850d60e94cc36656f0e8 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Mon, 2 Aug 2021 03:48:45 +0200 Subject: [PATCH 57/82] Address comments --- .../Probing/ProbeResultNormalizer.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index aa6674468b..cdfdbc5dad 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -28,7 +28,9 @@ namespace MediaBrowser.MediaEncoding.Probing private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' }; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private readonly Regex _performerPattern = new (@"(?.*) \((?.*)\)"); + + private readonly CultureInfo _usCulture = new ("en-US"); private readonly ILogger _logger; private readonly ILocalizationManager _localization; @@ -1116,8 +1118,7 @@ namespace MediaBrowser.MediaEncoding.Probing { foreach (var person in Split(performer, false)) { - Regex pattern = new Regex(@"(?.*) \((?.*)\)"); - Match match = pattern.Match(person); + Match match = _performerPattern.Match(person); // If the performer doesn't have any instrument/role associated, it won't match. In that case, chances are it's simply a band name, so we skip it. if (match.Success) From 7f52cda03c9731fcbd57fd8c9ed8806c6965d054 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Sat, 7 Aug 2021 21:58:19 +0200 Subject: [PATCH 58/82] Make performer regex static --- MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index cdfdbc5dad..e83e533c92 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -28,7 +28,7 @@ namespace MediaBrowser.MediaEncoding.Probing private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' }; - private readonly Regex _performerPattern = new (@"(?.*) \((?.*)\)"); + private static readonly Regex _performerPattern = new (@"(?.*) \((?.*)\)"); private readonly CultureInfo _usCulture = new ("en-US"); private readonly ILogger _logger; From 645825db36f2de9771aa5d0ebcdab25855d18b5d Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sat, 28 Aug 2021 17:32:09 +0200 Subject: [PATCH 59/82] Enable nullable for more files --- .../Collections/CollectionManager.cs | 2 +- .../Library/LibraryManager.cs | 5 +- .../LiveTv/EmbyTV/DirectRecorder.cs | 8 ++-- .../LiveTv/EmbyTV/EmbyTV.cs | 47 ++++++++----------- .../LiveTv/EmbyTV/EncodedRecorder.cs | 5 -- .../LiveTv/EmbyTV/EpgChannelData.cs | 1 - .../LiveTv/EmbyTV/IRecorder.cs | 2 +- .../LiveTv/EmbyTV/TimerManager.cs | 12 ++--- .../LiveTv/Listings/SchedulesDirect.cs | 2 +- .../Controllers/LibraryStructureController.cs | 4 +- .../Configuration/MediaPathInfo.cs | 8 +++- .../Configuration/NfoConfigurationFactory.cs | 4 +- 12 files changed, 42 insertions(+), 58 deletions(-) diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index 8270c2e84c..b00a519229 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -95,7 +95,7 @@ namespace Emby.Server.Implementations.Collections var libraryOptions = new LibraryOptions { - PathInfos = new[] { new MediaPathInfo { Path = path } }, + PathInfos = new[] { new MediaPathInfo(path) }, EnableRealtimeMonitor = false, SaveLocalMetadata = true }; diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 13fb8b2fd5..0cde8101c5 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -3173,10 +3173,7 @@ namespace Emby.Server.Implementations.Library { if (!list.Any(i => string.Equals(i.Path, location, StringComparison.Ordinal))) { - list.Add(new MediaPathInfo - { - Path = location - }); + list.Add(new MediaPathInfo(location)); } } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs index 3fcadf5b1b..bb3d635d12 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -33,7 +31,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return targetFile; } - public Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) + public Task Record(IDirectStreamProvider? directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { if (directStreamProvider != null) { @@ -45,7 +43,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private async Task RecordFromDirectStreamProvider(IDirectStreamProvider directStreamProvider, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { - Directory.CreateDirectory(Path.GetDirectoryName(targetFile)); + Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile))); // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None)) @@ -71,7 +69,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _logger.LogInformation("Opened recording stream from tuner provider"); - Directory.CreateDirectory(Path.GetDirectoryName(targetFile)); + Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile))); // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 7970631201..f2b9f3cb90 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -159,8 +159,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV try { var recordingFolders = GetRecordingFolders().ToArray(); - var virtualFolders = _libraryManager.GetVirtualFolders() - .ToList(); + var virtualFolders = _libraryManager.GetVirtualFolders(); var allExistingPaths = virtualFolders.SelectMany(i => i.Locations).ToList(); @@ -177,7 +176,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV continue; } - var mediaPathInfos = pathsToCreate.Select(i => new MediaPathInfo { Path = i }).ToArray(); + var mediaPathInfos = pathsToCreate.Select(i => new MediaPathInfo(i)).ToArray(); var libraryOptions = new LibraryOptions { @@ -210,7 +209,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV foreach (var path in pathsToRemove) { - await RemovePathFromLibrary(path).ConfigureAwait(false); + await RemovePathFromLibraryAsync(path).ConfigureAwait(false); } } catch (Exception ex) @@ -219,13 +218,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - private async Task RemovePathFromLibrary(string path) + private async Task RemovePathFromLibraryAsync(string path) { _logger.LogDebug("Removing path from library: {0}", path); var requiresRefresh = false; - var virtualFolders = _libraryManager.GetVirtualFolders() - .ToList(); + var virtualFolders = _libraryManager.GetVirtualFolders(); foreach (var virtualFolder in virtualFolders) { @@ -460,7 +458,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (!string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId)) { var tunerChannelId = tunerChannel.TunerChannelId; - if (tunerChannelId.IndexOf(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase) != -1) + if (tunerChannelId.Contains(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase)) { tunerChannelId = tunerChannelId.Replace(".json.schedulesdirect.org", string.Empty, StringComparison.OrdinalIgnoreCase).TrimStart('I'); } @@ -620,8 +618,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (existingTimer != null) { - if (existingTimer.Status == RecordingStatus.Cancelled || - existingTimer.Status == RecordingStatus.Completed) + if (existingTimer.Status == RecordingStatus.Cancelled + || existingTimer.Status == RecordingStatus.Completed) { existingTimer.Status = RecordingStatus.New; existingTimer.IsManual = true; @@ -913,18 +911,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var epgChannel = await GetEpgChannelFromTunerChannel(provider.Item1, provider.Item2, channel, cancellationToken).ConfigureAwait(false); - List programs; - if (epgChannel == null) { _logger.LogDebug("EPG channel not found for tuner channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty); - programs = new List(); + continue; } - else - { - programs = (await provider.Item1.GetProgramsAsync(provider.Item2, epgChannel.Id, startDateUtc, endDateUtc, cancellationToken) + + List programs = (await provider.Item1.GetProgramsAsync(provider.Item2, epgChannel.Id, startDateUtc, endDateUtc, cancellationToken) .ConfigureAwait(false)).ToList(); - } // Replace the value that came from the provider with a normalized value foreach (var program in programs) @@ -940,7 +934,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - return new List(); + return Enumerable.Empty(); } private List> GetListingProviders() @@ -1292,7 +1286,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _logger.LogInformation("Beginning recording. Will record for {0} minutes.", duration.TotalMinutes.ToString(CultureInfo.InvariantCulture)); - _logger.LogInformation("Writing file to path: " + recordPath); + _logger.LogInformation("Writing file to: {Path}", recordPath); Action onStarted = async () => { @@ -1417,13 +1411,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private void TriggerRefresh(string path) { - _logger.LogInformation("Triggering refresh on {path}", path); + _logger.LogInformation("Triggering refresh on {Path}", path); var item = GetAffectedBaseItem(Path.GetDirectoryName(path)); if (item != null) { - _logger.LogInformation("Refreshing recording parent {path}", item.Path); + _logger.LogInformation("Refreshing recording parent {Path}", item.Path); _providerManager.QueueRefresh( item.Id, @@ -1458,7 +1452,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (item.GetType() == typeof(Folder) && string.Equals(item.Path, parentPath, StringComparison.OrdinalIgnoreCase)) { var parentItem = item.GetParent(); - if (parentItem != null && !(parentItem is AggregateFolder)) + if (parentItem != null && parentItem is not AggregateFolder) { item = parentItem; } @@ -1512,8 +1506,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV DeleteLibraryItemsForTimers(timersToDelete); - var librarySeries = _libraryManager.FindByPath(seriesPath, true) as Folder; - if (librarySeries == null) + if (_libraryManager.FindByPath(seriesPath, true) is not Folder librarySeries) { return; } @@ -1667,7 +1660,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _logger.LogInformation("Running recording post processor {0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); - process.Exited += Process_Exited; + process.Exited += OnProcessExited; process.Start(); } catch (Exception ex) @@ -1681,7 +1674,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return arguments.Replace("{path}", path, StringComparison.OrdinalIgnoreCase); } - private void Process_Exited(object sender, EventArgs e) + private void OnProcessExited(object sender, EventArgs e) { using (var process = (Process)sender) { @@ -2239,7 +2232,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var enabledTimersForSeries = new List(); foreach (var timer in allTimers) { - var existingTimer = _timerProvider.GetTimer(timer.Id) + var existingTimer = _timerProvider.GetTimer(timer.Id) ?? (string.IsNullOrWhiteSpace(timer.ProgramId) ? null : _timerProvider.GetTimerByProgramId(timer.ProgramId)); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 93781cb7ba..e10bc76470 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -319,11 +319,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } } - catch (ObjectDisposedException) - { - // TODO Investigate and properly fix. - // Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux - } catch (Exception ex) { _logger.LogError(ex, "Error reading ffmpeg recording log"); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs index 0ec52a9598..20a8213a77 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs @@ -8,7 +8,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { internal class EpgChannelData { - private readonly Dictionary _channelsById; private readonly Dictionary _channelsByNumber; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs index 4712724d67..dfe3517b22 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs @@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV /// /// Records the specified media source. /// - Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken); + Task Record(IDirectStreamProvider? directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken); string GetOutputPath(MediaSourceInfo mediaSource, string targetFile); } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs index 6c52a9a73d..a861e6ae44 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -23,7 +21,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { } - public event EventHandler> TimerFired; + public event EventHandler>? TimerFired; public void RestartTimers() { @@ -145,9 +143,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - private void TimerCallback(object state) + private void TimerCallback(object? state) { - var timerId = (string)state; + var timerId = (string?)state ?? throw new ArgumentNullException(nameof(state)); var timer = GetAll().FirstOrDefault(i => string.Equals(i.Id, timerId, StringComparison.OrdinalIgnoreCase)); if (timer != null) @@ -156,12 +154,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - public TimerInfo GetTimer(string id) + public TimerInfo? GetTimer(string id) { return GetAll().FirstOrDefault(r => string.Equals(r.Id, id, StringComparison.OrdinalIgnoreCase)); } - public TimerInfo GetTimerByProgramId(string programId) + public TimerInfo? GetTimerByProgramId(string programId) { return GetAll().FirstOrDefault(r => string.Equals(r.ProgramId, programId, StringComparison.OrdinalIgnoreCase)); } diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index b7639a51ce..d029d56c87 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -14,8 +14,8 @@ using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common; using Jellyfin.Extensions.Json; +using MediaBrowser.Common; using MediaBrowser.Common.Net; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Cryptography; diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index be9127dd39..ec1170411c 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -84,7 +84,7 @@ namespace Jellyfin.Api.Controllers if (paths != null && paths.Length > 0) { - libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo { Path = i }).ToArray(); + libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo(i)).ToArray(); } await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false); @@ -212,7 +212,7 @@ namespace Jellyfin.Api.Controllers try { - var mediaPath = mediaPathDto.PathInfo ?? new MediaPathInfo { Path = mediaPathDto.Path }; + var mediaPath = mediaPathDto.PathInfo ?? new MediaPathInfo(mediaPathDto.Path ?? throw new ArgumentException("PathInfo and Path can't both be null.")); _libraryManager.AddMediaPath(mediaPathDto.Name, mediaPath); } diff --git a/MediaBrowser.Model/Configuration/MediaPathInfo.cs b/MediaBrowser.Model/Configuration/MediaPathInfo.cs index 4f311c58f0..d096defcb8 100644 --- a/MediaBrowser.Model/Configuration/MediaPathInfo.cs +++ b/MediaBrowser.Model/Configuration/MediaPathInfo.cs @@ -1,12 +1,16 @@ -#nullable disable #pragma warning disable CS1591 namespace MediaBrowser.Model.Configuration { public class MediaPathInfo { + public MediaPathInfo(string path) + { + Path = path; + } + public string Path { get; set; } - public string NetworkPath { get; set; } + public string? NetworkPath { get; set; } } } diff --git a/MediaBrowser.XbmcMetadata/Configuration/NfoConfigurationFactory.cs b/MediaBrowser.XbmcMetadata/Configuration/NfoConfigurationFactory.cs index 8325bfdbd5..c73989e761 100644 --- a/MediaBrowser.XbmcMetadata/Configuration/NfoConfigurationFactory.cs +++ b/MediaBrowser.XbmcMetadata/Configuration/NfoConfigurationFactory.cs @@ -15,8 +15,8 @@ namespace MediaBrowser.XbmcMetadata.Configuration { new ConfigurationStore { - ConfigurationType = typeof(XbmcMetadataOptions), - Key = "xbmcmetadata" + ConfigurationType = typeof(XbmcMetadataOptions), + Key = "xbmcmetadata" } }; } From 909573f2f142ca2f5e1e54093c820b9f9ebb6168 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sat, 28 Aug 2021 19:30:57 +0200 Subject: [PATCH 60/82] Add tests for LibraryStructureController --- .../MediaStructureControllerTests.cs | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs new file mode 100644 index 0000000000..9f189aaab9 --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs @@ -0,0 +1,83 @@ +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Text.Json; +using System.Threading.Tasks; +using Jellyfin.Api.Models.LibraryStructureDto; +using Jellyfin.Extensions.Json; +using Xunit; + +namespace Jellyfin.Server.Integration.Tests.Controllers +{ + public sealed class MediaStructureControllerTests : IClassFixture + { + private readonly JellyfinApplicationFactory _factory; + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; + private static string? _accessToken; + + public MediaStructureControllerTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task AddMediaPath_PathDoesntExist_ReturnsNotFound() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var data = new MediaPathDto() + { + Name = "Test", + Path = "/this/path/doesnt/exist" + }; + + using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(data, _jsonOptions)); + postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json); + var response = await client.PostAsync("Library/VirtualFolders/Paths", postContent).ConfigureAwait(false); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task UpdateMediaPath_EmptyName_ReturnsBadRequest() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var data = new UpdateMediaPathRequestDto() + { + Name = string.Empty, + }; + + using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(data, _jsonOptions)); + postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json); + var response = await client.PostAsync("Library/VirtualFolders/Paths/Update", postContent).ConfigureAwait(false); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task RemoveMediaPath_EmptyName_ReturnsBadRequest() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=").ConfigureAwait(false); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task RemoveMediaPath_PathDoesntExist_ReturnsNotFound() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=none&path=%2Fthis%2Fpath%2Fdoesnt%2Fexist").ConfigureAwait(false); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + } +} From 69ba539c700c502f189c9067c31860c889aed1f7 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sat, 28 Aug 2021 20:12:25 +0200 Subject: [PATCH 61/82] Add more tests for LibraryStructureController --- .../MediaStructureControllerTests.cs | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs index 9f189aaab9..5855f0266f 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs @@ -1,3 +1,4 @@ +using System; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -21,6 +22,42 @@ namespace Jellyfin.Server.Integration.Tests.Controllers _factory = factory; } + [Fact] + public async Task RenameVirtualFolder_WhiteSpaceName_ReturnsBadRequest() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + using var postContent = new ByteArrayContent(Array.Empty()); + var response = await client.PostAsync("Library/VirtualFolders/Name?name=+&newName=test", postContent).ConfigureAwait(false); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task RenameVirtualFolder_WhiteSpaceNewName_ReturnsBadRequest() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + using var postContent = new ByteArrayContent(Array.Empty()); + var response = await client.PostAsync("Library/VirtualFolders/Name?name=test&newName=+", postContent).ConfigureAwait(false); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task RenameVirtualFolder_NameDoesntExist_ReturnsNotFound() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + using var postContent = new ByteArrayContent(Array.Empty()); + var response = await client.PostAsync("Library/VirtualFolders/Name?name=doesnt+exist&newName=test", postContent).ConfigureAwait(false); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + [Fact] public async Task AddMediaPath_PathDoesntExist_ReturnsNotFound() { @@ -59,12 +96,12 @@ namespace Jellyfin.Server.Integration.Tests.Controllers } [Fact] - public async Task RemoveMediaPath_EmptyName_ReturnsBadRequest() + public async Task RemoveMediaPath_WhiteSpaceName_ReturnsBadRequest() { var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); - var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=").ConfigureAwait(false); + var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=+").ConfigureAwait(false); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } From 0f7c875076e29f0c86c536840c71cec29fab08fb Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sat, 28 Aug 2021 20:34:04 +0200 Subject: [PATCH 62/82] Fix test --- .../Controllers/MediaStructureControllerTests.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs index 5855f0266f..8af43050f6 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs @@ -7,6 +7,7 @@ using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Api.Models.LibraryStructureDto; using Jellyfin.Extensions.Json; +using MediaBrowser.Model.Configuration; using Xunit; namespace Jellyfin.Server.Integration.Tests.Controllers @@ -78,14 +79,18 @@ namespace Jellyfin.Server.Integration.Tests.Controllers } [Fact] - public async Task UpdateMediaPath_EmptyName_ReturnsBadRequest() + public async Task UpdateMediaPath_WhiteSpaceName_ReturnsBadRequest() { var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); var data = new UpdateMediaPathRequestDto() { - Name = string.Empty, + Name = " ", + PathInfo = new MediaPathInfo + { + Path = "test" + } }; using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(data, _jsonOptions)); From cba07b1ca6862ef8450021b0e60ce2f366ff0d33 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Sat, 28 Aug 2021 16:32:50 -0600 Subject: [PATCH 63/82] Remove more and more warnings --- Emby.Dlna/ContentDirectory/ServerItem.cs | 2 +- Emby.Dlna/Didl/DidlBuilder.cs | 2 +- .../ApplicationHost.cs | 3 +- .../Channels/ChannelManager.cs | 6 +- .../Data/BaseSqliteRepository.cs | 2 +- .../Data/SqliteItemRepository.cs | 9 +- .../Data/SqliteUserDataRepository.cs | 8 +- Emby.Server.Implementations/Dto/DtoService.cs | 41 +- .../EntryPoints/LibraryChangedNotifier.cs | 4 +- .../EntryPoints/UdpServerEntryPoint.cs | 3 + .../Security/AuthorizationContext.cs | 2 + .../HttpServer/WebSocketConnection.cs | 4 +- .../IStartupOptions.cs | 2 +- .../Images/CollectionFolderImageProvider.cs | 12 +- .../Library/LibraryManager.cs | 8 +- .../Library/Resolvers/Audio/AudioResolver.cs | 12 +- .../Library/Resolvers/BaseVideoResolver.cs | 26 +- ...deoResolver.cs => GenericVideoResolver.cs} | 0 .../Library/Resolvers/ItemResolver.cs | 2 +- .../Library/Resolvers/Movies/MovieResolver.cs | 4 +- .../Library/Resolvers/PlaylistResolver.cs | 3 +- .../Library/Validators/StudiosValidator.cs | 9 +- .../LiveTv/EmbyTV/EmbyTV.cs | 2 +- .../LiveTv/EmbyTV/EpgChannelData.cs | 1 - .../LiveTv/Listings/SchedulesDirect.cs | 620 ++++-------------- .../SchedulesDirectDtos/BroadcasterDto.cs | 36 + .../SchedulesDirectDtos/CaptionDto.cs | 24 + .../Listings/SchedulesDirectDtos/CastDto.cs | 48 ++ .../SchedulesDirectDtos/ChannelDto.cs | 31 + .../SchedulesDirectDtos/ContentRatingDto.cs | 24 + .../Listings/SchedulesDirectDtos/CrewDto.cs | 42 ++ .../Listings/SchedulesDirectDtos/DayDto.cs | 39 ++ .../SchedulesDirectDtos/Description1000Dto.cs | 24 + .../SchedulesDirectDtos/Description100Dto.cs | 24 + .../DescriptionsProgramDto.cs | 25 + .../SchedulesDirectDtos/EventDetailsDto.cs | 18 + .../SchedulesDirectDtos/GracenoteDto.cs | 24 + .../SchedulesDirectDtos/HeadendsDto.cs | 37 ++ .../SchedulesDirectDtos/ImageDataDto.cs | 69 ++ .../Listings/SchedulesDirectDtos/LineupDto.cs | 42 ++ .../SchedulesDirectDtos/LineupsDto.cs | 37 ++ .../Listings/SchedulesDirectDtos/LogoDto.cs | 36 + .../Listings/SchedulesDirectDtos/MapDto.cs | 48 ++ .../SchedulesDirectDtos/MetadataDto.cs | 30 + .../MetadataProgramsDto.cs | 18 + .../MetadataScheduleDto.cs | 42 ++ .../Listings/SchedulesDirectDtos/MovieDto.cs | 31 + .../SchedulesDirectDtos/MultipartDto.cs | 24 + .../SchedulesDirectDtos/ProgramDetailsDto.cs | 157 +++++ .../SchedulesDirectDtos/ProgramDto.cs | 91 +++ .../SchedulesDirectDtos/QualityRatingDto.cs | 42 ++ .../Listings/SchedulesDirectDtos/RatingDto.cs | 24 + .../SchedulesDirectDtos/RecommendationDto.cs | 24 + .../RequestScheduleForChannelDto.cs | 25 + .../SchedulesDirectDtos/ShowImagesDto.cs | 22 + .../SchedulesDirectDtos/StationDto.cs | 67 ++ .../Listings/SchedulesDirectDtos/TitleDto.cs | 18 + .../Listings/SchedulesDirectDtos/TokenDto.cs | 36 + .../LiveTv/LiveTvManager.cs | 6 +- .../TunerHosts/HdHomerun/HdHomerunManager.cs | 1 + .../Playlists/PlaylistManager.cs | 4 +- .../ScheduledTasks/ScheduledTaskWorker.cs | 3 +- .../Sorting/ArtistComparer.cs | 2 +- .../Controllers/ItemUpdateController.cs | 10 +- Jellyfin.Api/Controllers/ItemsController.cs | 4 +- Jellyfin.Api/Controllers/LibraryController.cs | 2 +- Jellyfin.Api/Controllers/TvShowsController.cs | 8 +- Jellyfin.Api/Helpers/StreamingHelpers.cs | 4 - MediaBrowser.Controller/Entities/BaseItem.cs | 4 +- MediaBrowser.Controller/Entities/Folder.cs | 10 +- MediaBrowser.Controller/Entities/TV/Series.cs | 2 +- .../MediaEncoding/EncodingHelper.cs | 12 +- .../MediaEncoding/EncodingJobInfo.cs | 6 +- .../Images/LocalImageProvider.cs | 2 +- .../Savers/BaseXmlSaver.cs | 8 +- MediaBrowser.Model/Dlna/MediaFormatProfile.cs | 2 +- .../Dlna/ResolutionNormalizer.cs | 4 - MediaBrowser.Model/Dlna/StreamBuilder.cs | 33 +- MediaBrowser.Model/Dlna/StreamInfo.cs | 2 +- MediaBrowser.Model/Entities/MediaStream.cs | 2 +- MediaBrowser.Model/IO/IFileSystem.cs | 4 +- MediaBrowser.Providers/Manager/ImageSaver.cs | 2 +- .../Manager/ItemImageProvider.cs | 4 +- .../Manager/MetadataService.cs | 6 +- .../Manager/ProviderManager.cs | 10 +- .../Manager/ProviderUtils.cs | 2 +- .../Savers/BaseNfoSaver.cs | 6 +- .../Savers/MovieNfoSaver.cs | 2 +- .../Savers/SeasonNfoSaver.cs | 2 +- 89 files changed, 1554 insertions(+), 681 deletions(-) rename Emby.Server.Implementations/Library/Resolvers/{VideoResolver.cs => GenericVideoResolver.cs} (100%) create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs create mode 100644 Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs diff --git a/Emby.Dlna/ContentDirectory/ServerItem.cs b/Emby.Dlna/ContentDirectory/ServerItem.cs index 34244000c1..ff30e6e4af 100644 --- a/Emby.Dlna/ContentDirectory/ServerItem.cs +++ b/Emby.Dlna/ContentDirectory/ServerItem.cs @@ -17,7 +17,7 @@ namespace Emby.Dlna.ContentDirectory { Item = item; - if (item is IItemByName && !(item is Folder)) + if (item is IItemByName && item is not Folder) { StubType = Dlna.ContentDirectory.StubType.Folder; } diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index 2982ce97e1..c000784997 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -748,7 +748,7 @@ namespace Emby.Dlna.Didl AddValue(writer, "upnp", "publisher", studio, NsUpnp); } - if (!(item is Folder)) + if (item is not Folder) { if (filter.Contains("dc:description")) { diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index bf7ddace2d..b640f06c6e 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -456,6 +456,7 @@ namespace Emby.Server.Implementations /// /// Runs the startup tasks. /// + /// The cancellation token. /// . public async Task RunStartupTasksAsync(CancellationToken cancellationToken) { @@ -469,7 +470,7 @@ namespace Emby.Server.Implementations _mediaEncoder.SetFFmpegPath(); - Logger.LogInformation("ServerId: {0}", SystemId); + Logger.LogInformation("ServerId: {ServerId}", SystemId); var entryPoints = GetExports(); diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index aa54510a71..41d1f9b392 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -102,7 +102,7 @@ namespace Emby.Server.Implementations.Channels var internalChannel = _libraryManager.GetItemById(item.ChannelId); var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id)); - return !(channel is IDisableMediaSourceDisplay); + return channel is not IDisableMediaSourceDisplay; } /// @@ -1079,11 +1079,11 @@ namespace Emby.Server.Implementations.Channels // was used for status // if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal)) - //{ + // { // item.ExternalEtag = info.Etag; // forceUpdate = true; // _logger.LogDebug("Forcing update due to ExternalEtag {0}", item.Name); - //} + // } if (!internalChannelId.Equals(item.ChannelId)) { diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 6f23a0888e..01c9fbca81 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.Data protected virtual int? CacheSize => null; /// - /// Gets the journal mode. + /// Gets the journal mode. . /// /// The journal mode. protected virtual string JournalMode => "TRUNCATE"; diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 2cb10765ff..ab1b55164b 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -72,9 +72,16 @@ namespace Emby.Server.Implementations.Data _mediaAttachmentInsertPrefix = queryPrefixText.ToString(); } + /// /// 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. + /// config is null. public SqliteItemRepository( IServerConfigurationManager config, IServerApplicationHost appHost, @@ -4879,7 +4886,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type foreach (var t in _knownTypes) { - dict[t.Name] = t.FullName ; + dict[t.Name] = t.FullName; } dict["Program"] = typeof(LiveTvProgram).FullName; diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index ef9af1dcd0..613d07d775 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -174,7 +174,6 @@ namespace Emby.Server.Implementations.Data /// The key. /// The user data. /// The cancellation token. - /// Task. public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -319,8 +318,8 @@ namespace Emby.Server.Implementations.Data /// /// Return all user-data associated with the given user. /// - /// - /// + /// The internal user id. + /// The list of user item data. public List GetAllUserData(long internalUserId) { if (internalUserId <= 0) @@ -349,7 +348,8 @@ namespace Emby.Server.Implementations.Data /// /// Read a row from the specified reader into the provided userData object. /// - /// + /// The list of result set values. + /// The user item data. private UserItemData ReadRow(IReadOnlyList reader) { var userData = new UserItemData(); diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 7411239a1e..7c2d02037c 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -807,7 +807,7 @@ namespace Emby.Server.Implementations.Dto dto.MediaType = item.MediaType; - if (!(item is LiveTvProgram)) + if (item is not LiveTvProgram) { dto.LocationType = item.LocationType; } @@ -928,9 +928,9 @@ namespace Emby.Server.Implementations.Dto } // if (options.ContainsField(ItemFields.MediaSourceCount)) - //{ + // { // Songs always have one - //} + // } } if (item is IHasArtist hasArtist) @@ -938,10 +938,10 @@ namespace Emby.Server.Implementations.Dto dto.Artists = hasArtist.Artists; // var artistItems = _libraryManager.GetArtists(new InternalItemsQuery - //{ + // { // EnableTotalRecordCount = false, // ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) } - //}); + // }); // dto.ArtistItems = artistItems.Items // .Select(i => @@ -958,7 +958,7 @@ namespace Emby.Server.Implementations.Dto // Include artists that are not in the database yet, e.g., just added via metadata editor // var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList(); dto.ArtistItems = hasArtist.Artists - //.Except(foundArtists, new DistinctNameComparer()) + // .Except(foundArtists, new DistinctNameComparer()) .Select(i => { // This should not be necessary but we're seeing some cases of it @@ -990,10 +990,10 @@ namespace Emby.Server.Implementations.Dto dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault(); // var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery - //{ + // { // EnableTotalRecordCount = false, // ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) } - //}); + // }); // dto.AlbumArtists = artistItems.Items // .Select(i => @@ -1008,7 +1008,7 @@ namespace Emby.Server.Implementations.Dto // .ToList(); dto.AlbumArtists = hasAlbumArtist.AlbumArtists - //.Except(foundArtists, new DistinctNameComparer()) + // .Except(foundArtists, new DistinctNameComparer()) .Select(i => { // This should not be necessary but we're seeing some cases of it @@ -1035,8 +1035,7 @@ namespace Emby.Server.Implementations.Dto } // Add video info - var video = item as Video; - if (video != null) + if (item is Video video) { dto.VideoType = video.VideoType; dto.Video3DFormat = video.Video3DFormat; @@ -1075,9 +1074,7 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.MediaStreams)) { // Add VideoInfo - var iHasMediaSources = item as IHasMediaSources; - - if (iHasMediaSources != null) + if (item is IHasMediaSources) { MediaStream[] mediaStreams; @@ -1146,7 +1143,7 @@ namespace Emby.Server.Implementations.Dto // TODO maybe remove the if statement entirely // if (options.ContainsField(ItemFields.SeriesPrimaryImage)) { - episodeSeries = episodeSeries ?? episode.Series; + episodeSeries ??= episode.Series; if (episodeSeries != null) { dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary); @@ -1159,7 +1156,7 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.SeriesStudio)) { - episodeSeries = episodeSeries ?? episode.Series; + episodeSeries ??= episode.Series; if (episodeSeries != null) { dto.SeriesStudio = episodeSeries.Studios.FirstOrDefault(); @@ -1172,7 +1169,7 @@ namespace Emby.Server.Implementations.Dto { dto.AirDays = series.AirDays; dto.AirTime = series.AirTime; - dto.Status = series.Status.HasValue ? series.Status.Value.ToString() : null; + dto.Status = series.Status?.ToString(); } // Add SeasonInfo @@ -1185,7 +1182,7 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.SeriesStudio)) { - series = series ?? season.Series; + series ??= season.Series; if (series != null) { dto.SeriesStudio = series.Studios.FirstOrDefault(); @@ -1196,7 +1193,7 @@ namespace Emby.Server.Implementations.Dto // TODO maybe remove the if statement entirely // if (options.ContainsField(ItemFields.SeriesPrimaryImage)) { - series = series ?? season.Series; + series ??= season.Series; if (series != null) { dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary); @@ -1283,7 +1280,7 @@ namespace Emby.Server.Implementations.Dto var parent = currentItem.DisplayParent ?? currentItem.GetOwner() ?? currentItem.GetParent(); - if (parent == null && !(originalItem is UserRootFolder) && !(originalItem is UserView) && !(originalItem is AggregateFolder) && !(originalItem is ICollectionFolder) && !(originalItem is Channel)) + if (parent == null && originalItem is not UserRootFolder && originalItem is not UserView && originalItem is not AggregateFolder && originalItem is not ICollectionFolder && originalItem is not Channel) { parent = _libraryManager.GetCollectionFolders(originalItem).FirstOrDefault(); } @@ -1317,7 +1314,7 @@ namespace Emby.Server.Implementations.Dto var imageTags = dto.ImageTags; while (((!(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0) || (!(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0) || (!(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0) || parent is Series) && - (parent = parent ?? (isFirst ? GetImageDisplayParent(item, item) ?? owner : parent)) != null) + (parent ??= (isFirst ? GetImageDisplayParent(item, item) ?? owner : parent)) != null) { if (parent == null) { @@ -1348,7 +1345,7 @@ namespace Emby.Server.Implementations.Dto } } - if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && !(parent is ICollectionFolder) && !(parent is UserView)) + if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && parent is not ICollectionFolder && parent is not UserView) { var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb); diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 5bb4100ba9..df48346e3c 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -149,7 +149,7 @@ namespace Emby.Server.Implementations.EntryPoints private static bool EnableRefreshMessage(BaseItem item) { - if (!(item is Folder folder)) + if (item is not Folder folder) { return false; } @@ -403,7 +403,7 @@ namespace Emby.Server.Implementations.EntryPoints return false; } - if (item is IItemByName && !(item is MusicArtist)) + if (item is IItemByName && item is not MusicArtist) { return false; } diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 2e72b18f57..feaccf9fa1 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -37,6 +37,9 @@ namespace Emby.Server.Implementations.EntryPoints /// /// Initializes a new instance of the class. /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. public UdpServerEntryPoint( ILogger logger, IServerApplicationHost appHost, diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs index b2625a68c9..badc6ce6c6 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -74,6 +74,7 @@ namespace Emby.Server.Implementations.HttpServer.Security auth.TryGetValue("Token", out token); } +#pragma warning disable CA1508 // string.IsNullOrEmpty(token) is always false. if (string.IsNullOrEmpty(token)) { token = headers["X-Emby-Token"]; @@ -111,6 +112,7 @@ namespace Emby.Server.Implementations.HttpServer.Security // Request doesn't contain a token. return authInfo; } +#pragma warning restore CA1508 authInfo.HasToken = true; var result = _authRepo.Get(new AuthenticationInfoQuery diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index 5d38ea0ca6..7010a6fb08 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.HttpServer public event EventHandler? Closed; /// - /// Gets or sets the remote end point. + /// Gets the remote end point. /// public IPAddress? RemoteEndPoint { get; } @@ -82,7 +82,7 @@ namespace Emby.Server.Implementations.HttpServer public DateTime LastKeepAliveDate { get; set; } /// - /// Gets or sets the query string. + /// Gets the query string. /// /// The query string. public IQueryCollection QueryString { get; } diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs index a430b9e720..1d97882db5 100644 --- a/Emby.Server.Implementations/IStartupOptions.cs +++ b/Emby.Server.Implementations/IStartupOptions.cs @@ -10,7 +10,7 @@ namespace Emby.Server.Implementations string? FFmpegPath { get; } /// - /// Gets the value of the --service command line option. + /// Gets a value value indicating whether to run as service by the --service command line option. /// bool IsService { get; } diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs index ff5f26ce09..0229fbae79 100644 --- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs @@ -30,27 +30,27 @@ namespace Emby.Server.Implementations.Images string[] includeItemTypes; - if (string.Equals(viewType, CollectionType.Movies)) + if (string.Equals(viewType, CollectionType.Movies, StringComparison.Ordinal)) { includeItemTypes = new string[] { "Movie" }; } - else if (string.Equals(viewType, CollectionType.TvShows)) + else if (string.Equals(viewType, CollectionType.TvShows, StringComparison.Ordinal)) { includeItemTypes = new string[] { "Series" }; } - else if (string.Equals(viewType, CollectionType.Music)) + else if (string.Equals(viewType, CollectionType.Music, StringComparison.Ordinal)) { includeItemTypes = new string[] { "MusicAlbum" }; } - else if (string.Equals(viewType, CollectionType.Books)) + else if (string.Equals(viewType, CollectionType.Books, StringComparison.Ordinal)) { includeItemTypes = new string[] { "Book", "AudioBook" }; } - else if (string.Equals(viewType, CollectionType.BoxSets)) + else if (string.Equals(viewType, CollectionType.BoxSets, StringComparison.Ordinal)) { includeItemTypes = new string[] { "BoxSet" }; } - else if (string.Equals(viewType, CollectionType.HomeVideos) || string.Equals(viewType, CollectionType.Photos)) + else if (string.Equals(viewType, CollectionType.HomeVideos, StringComparison.Ordinal) || string.Equals(viewType, CollectionType.Photos, StringComparison.Ordinal)) { includeItemTypes = new string[] { "Video", "Photo" }; } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 13fb8b2fd5..6a2983d9ba 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -287,14 +287,14 @@ namespace Emby.Server.Implementations.Library if (item is IItemByName) { - if (!(item is MusicArtist)) + if (item is not MusicArtist) { return; } } else if (!item.IsFolder) { - if (!(item is Video) && !(item is LiveTvChannel)) + if (item is not Video && item is not LiveTvChannel) { return; } @@ -866,7 +866,7 @@ namespace Emby.Server.Implementations.Library { var path = Person.GetPath(name); var id = GetItemByNameId(path); - if (!(GetItemById(id) is Person item)) + if (GetItemById(id) is not Person item) { item = new Person { @@ -2118,7 +2118,7 @@ namespace Emby.Server.Implementations.Library public LibraryOptions GetLibraryOptions(BaseItem item) { - if (!(item is CollectionFolder collectionFolder)) + if (item is not CollectionFolder collectionFolder) { // List.Find is more performant than FirstOrDefault due to enumerator allocation collectionFolder = GetCollectionFolders(item) diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index e893d63350..fd9747b4b7 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -21,11 +21,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// public class AudioResolver : ItemResolver, IMultiItemResolver { - private readonly ILibraryManager LibraryManager; + private readonly ILibraryManager _libraryManager; public AudioResolver(ILibraryManager libraryManager) { - LibraryManager = libraryManager; + _libraryManager = libraryManager; } /// @@ -88,13 +88,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio } var files = args.FileSystemChildren - .Where(i => !LibraryManager.IgnoreFile(i, args.Parent)) + .Where(i => !_libraryManager.IgnoreFile(i, args.Parent)) .ToList(); return FindAudio(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false); } - if (LibraryManager.IsAudioFile(args.Path)) + if (_libraryManager.IsAudioFile(args.Path)) { var extension = Path.GetExtension(args.Path); @@ -107,7 +107,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio var isMixedCollectionType = string.IsNullOrEmpty(collectionType); // For conflicting extensions, give priority to videos - if (isMixedCollectionType && LibraryManager.IsVideoFile(args.Path)) + if (isMixedCollectionType && _libraryManager.IsVideoFile(args.Path)) { return null; } @@ -182,7 +182,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio } } - var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); + var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(); var resolver = new AudioBookListResolver(namingOptions); var resolverResult = resolver.Resolve(files).ToList(); diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index cdb492022b..e003328089 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers /// /// Resolves a Path into a Video or Video subclass. /// - /// + /// The type of item to resolve. public abstract class BaseVideoResolver : MediaBrowser.Controller.Resolvers.ItemResolver where T : Video, new() { @@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Resolvers break; } - if (IsBluRayDirectory(child.FullName, filename, args.DirectoryService)) + if (IsBluRayDirectory(filename)) { videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions); @@ -279,25 +279,13 @@ namespace Emby.Server.Implementations.Library.Resolvers } /// - /// Determines whether [is blu ray directory] [the specified directory name]. + /// Determines whether [is bluray directory] [the specified directory name]. /// - protected bool IsBluRayDirectory(string fullPath, string directoryName, IDirectoryService directoryService) + /// The directory name. + /// Whether the directory is a bluray directory. + protected bool IsBluRayDirectory(string directoryName) { - if (!string.Equals(directoryName, "bdmv", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - return true; - // var blurayExtensions = new[] - //{ - // ".mts", - // ".m2ts", - // ".bdmv", - // ".mpls" - //}; - - // return directoryService.GetFiles(fullPath).Any(i => blurayExtensions.Contains(i.Extension ?? string.Empty, StringComparer.OrdinalIgnoreCase)); + return string.Equals(directoryName, "bdmv", StringComparison.OrdinalIgnoreCase); } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs similarity index 100% rename from Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs rename to Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs diff --git a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs index fa45ccf840..3f29ab191f 100644 --- a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs @@ -9,7 +9,7 @@ namespace Emby.Server.Implementations.Library.Resolvers /// /// Class ItemResolver. /// - /// + /// The type of BaseItem. public abstract class ItemResolver : IItemResolver where T : BaseItem, new() { diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 889e29a6ba..8b55a77449 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -400,7 +400,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return movie; } - if (IsBluRayDirectory(child.FullName, filename, directoryService)) + if (IsBluRayDirectory(filename)) { var movie = new T { @@ -481,7 +481,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return true; } - if (subfolders.Any(s => IsBluRayDirectory(s.FullName, s.Name, directoryService))) + if (subfolders.Any(s => IsBluRayDirectory(s.Name))) { videoTypes.Add(VideoType.BluRay); return true; diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs index ecd44be477..2c4ead7198 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs @@ -18,7 +18,8 @@ namespace Emby.Server.Implementations.Library.Resolvers /// public class PlaylistResolver : FolderResolver { - private string[] _musicPlaylistCollectionTypes = new string[] { + private string[] _musicPlaylistCollectionTypes = + { string.Empty, CollectionType.Music }; diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs index 9a8c5f39d5..16bdf720ce 100644 --- a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs @@ -87,12 +87,15 @@ namespace Emby.Server.Implementations.Library.Validators foreach (var item in deadEntities) { - _logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name); + _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name); - _libraryManager.DeleteItem(item, new DeleteOptions + _libraryManager.DeleteItem( + item, + new DeleteOptions { DeleteFileLocation = false - }, false); + }, + false); } progress.Report(100); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 7970631201..a6be407450 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -1458,7 +1458,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (item.GetType() == typeof(Folder) && string.Equals(item.Path, parentPath, StringComparison.OrdinalIgnoreCase)) { var parentItem = item.GetParent(); - if (parentItem != null && !(parentItem is AggregateFolder)) + if (parentItem != null && parentItem is not AggregateFolder) { item = parentItem; } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs index 0ec52a9598..20a8213a77 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs @@ -8,7 +8,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { internal class EpgChannelData { - private readonly Dictionary _channelsById; private readonly Dictionary _channelsByNumber; diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index b7639a51ce..bcfc4011c5 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -14,6 +14,7 @@ using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos; using MediaBrowser.Common; using Jellyfin.Extensions.Json; using MediaBrowser.Common.Net; @@ -96,12 +97,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings var dates = GetScheduleRequestDates(startDateUtc, endDateUtc); _logger.LogInformation("Channel Station ID is: {ChannelID}", channelId); - var requestList = new List() + var requestList = new List() { - new ScheduleDirect.RequestScheduleForChannel() + new RequestScheduleForChannelDto() { - stationID = channelId, - date = dates + StationId = channelId, + Date = dates } }; @@ -113,61 +114,61 @@ namespace Emby.Server.Implementations.LiveTv.Listings options.Headers.TryAddWithoutValidation("token", token); using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false); await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var dailySchedules = await JsonSerializer.DeserializeAsync>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false); + var dailySchedules = await JsonSerializer.DeserializeAsync>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false); _logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId); using var programRequestOptions = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/programs"); programRequestOptions.Headers.TryAddWithoutValidation("token", token); - var programsID = dailySchedules.SelectMany(d => d.programs.Select(s => s.programID)).Distinct(); + var programsID = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct(); programRequestOptions.Content = new StringContent("[\"" + string.Join("\", \"", programsID) + "\"]", Encoding.UTF8, MediaTypeNames.Application.Json); using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false); await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var programDetails = await JsonSerializer.DeserializeAsync>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false); - var programDict = programDetails.ToDictionary(p => p.programID, y => y); + var programDetails = await JsonSerializer.DeserializeAsync>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false); + var programDict = programDetails.ToDictionary(p => p.ProgramId, y => y); var programIdsWithImages = programDetails - .Where(p => p.hasImageArtwork).Select(p => p.programID) + .Where(p => p.HasImageArtwork).Select(p => p.ProgramId) .ToList(); var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false); var programsInfo = new List(); - foreach (ScheduleDirect.Program schedule in dailySchedules.SelectMany(d => d.programs)) + foreach (ProgramDto schedule in dailySchedules.SelectMany(d => d.Programs)) { // _logger.LogDebug("Proccesing Schedule for statio ID " + stationID + // " which corresponds to channel " + channelNumber + " and program id " + - // schedule.programID + " which says it has images? " + - // programDict[schedule.programID].hasImageArtwork); + // schedule.ProgramId + " which says it has images? " + + // programDict[schedule.ProgramId].hasImageArtwork); if (images != null) { - var imageIndex = images.FindIndex(i => i.programID == schedule.programID.Substring(0, 10)); + var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId[..10]); if (imageIndex > -1) { - var programEntry = programDict[schedule.programID]; + var programEntry = programDict[schedule.ProgramId]; - var allImages = images[imageIndex].data ?? new List(); - var imagesWithText = allImages.Where(i => string.Equals(i.text, "yes", StringComparison.OrdinalIgnoreCase)); - var imagesWithoutText = allImages.Where(i => string.Equals(i.text, "no", StringComparison.OrdinalIgnoreCase)); + var allImages = images[imageIndex].Data ?? new List(); + var imagesWithText = allImages.Where(i => string.Equals(i.Text, "yes", StringComparison.OrdinalIgnoreCase)); + var imagesWithoutText = allImages.Where(i => string.Equals(i.Text, "no", StringComparison.OrdinalIgnoreCase)); const double DesiredAspect = 2.0 / 3; - programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ?? + programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ?? GetProgramImage(ApiUrl, allImages, true, DesiredAspect); const double WideAspect = 16.0 / 9; - programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect); + programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect); // Don't supply the same image twice - if (string.Equals(programEntry.primaryImage, programEntry.thumbImage, StringComparison.Ordinal)) + if (string.Equals(programEntry.PrimaryImage, programEntry.ThumbImage, StringComparison.Ordinal)) { - programEntry.thumbImage = null; + programEntry.ThumbImage = null; } - programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect); + programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect); // programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ?? // GetProgramImage(ApiUrl, data, "Banner-L1", false) ?? @@ -176,15 +177,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings } } - programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.programID])); + programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.ProgramId])); } return programsInfo; } - private static int GetSizeOrder(ScheduleDirect.ImageData image) + private static int GetSizeOrder(ImageDataDto image) { - if (int.TryParse(image.height, out int value)) + if (int.TryParse(image.Height, out int value)) { return value; } @@ -192,53 +193,53 @@ namespace Emby.Server.Implementations.LiveTv.Listings return 0; } - private static string GetChannelNumber(ScheduleDirect.Map map) + private static string GetChannelNumber(MapDto map) { - var channelNumber = map.logicalChannelNumber; + var channelNumber = map.LogicalChannelNumber; if (string.IsNullOrWhiteSpace(channelNumber)) { - channelNumber = map.channel; + channelNumber = map.Channel; } if (string.IsNullOrWhiteSpace(channelNumber)) { - channelNumber = map.atscMajor + "." + map.atscMinor; + channelNumber = map.AtscMajor + "." + map.AtscMinor; } return channelNumber.TrimStart('0'); } - private static bool IsMovie(ScheduleDirect.ProgramDetails programInfo) + private static bool IsMovie(ProgramDetailsDto programInfo) { - return string.Equals(programInfo.entityType, "movie", StringComparison.OrdinalIgnoreCase); + return string.Equals(programInfo.EntityType, "movie", StringComparison.OrdinalIgnoreCase); } - private ProgramInfo GetProgram(string channelId, ScheduleDirect.Program programInfo, ScheduleDirect.ProgramDetails details) + private ProgramInfo GetProgram(string channelId, ProgramDto programInfo, ProgramDetailsDto details) { - var startAt = GetDate(programInfo.airDateTime); - var endAt = startAt.AddSeconds(programInfo.duration); + var startAt = GetDate(programInfo.AirDateTime); + var endAt = startAt.AddSeconds(programInfo.Duration); var audioType = ProgramAudio.Stereo; - var programId = programInfo.programID ?? string.Empty; + var programId = programInfo.ProgramId ?? string.Empty; string newID = programId + "T" + startAt.Ticks + "C" + channelId; - if (programInfo.audioProperties != null) + if (programInfo.AudioProperties != null) { - if (programInfo.audioProperties.Exists(item => string.Equals(item, "atmos", StringComparison.OrdinalIgnoreCase))) + if (programInfo.AudioProperties.Exists(item => string.Equals(item, "atmos", StringComparison.OrdinalIgnoreCase))) { audioType = ProgramAudio.Atmos; } - else if (programInfo.audioProperties.Exists(item => string.Equals(item, "dd 5.1", StringComparison.OrdinalIgnoreCase))) + else if (programInfo.AudioProperties.Exists(item => string.Equals(item, "dd 5.1", StringComparison.OrdinalIgnoreCase))) { audioType = ProgramAudio.DolbyDigital; } - else if (programInfo.audioProperties.Exists(item => string.Equals(item, "dd", StringComparison.OrdinalIgnoreCase))) + else if (programInfo.AudioProperties.Exists(item => string.Equals(item, "dd", StringComparison.OrdinalIgnoreCase))) { audioType = ProgramAudio.DolbyDigital; } - else if (programInfo.audioProperties.Exists(item => string.Equals(item, "stereo", StringComparison.OrdinalIgnoreCase))) + else if (programInfo.AudioProperties.Exists(item => string.Equals(item, "stereo", StringComparison.OrdinalIgnoreCase))) { audioType = ProgramAudio.Stereo; } @@ -249,9 +250,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings } string episodeTitle = null; - if (details.episodeTitle150 != null) + if (details.EpisodeTitle150 != null) { - episodeTitle = details.episodeTitle150; + episodeTitle = details.EpisodeTitle150; } var info = new ProgramInfo @@ -260,22 +261,22 @@ namespace Emby.Server.Implementations.LiveTv.Listings Id = newID, StartDate = startAt, EndDate = endAt, - Name = details.titles[0].title120 ?? "Unknown", + Name = details.Titles[0].Title120 ?? "Unknown", OfficialRating = null, CommunityRating = null, EpisodeTitle = episodeTitle, Audio = audioType, // IsNew = programInfo.@new ?? false, - IsRepeat = programInfo.@new == null, - IsSeries = string.Equals(details.entityType, "episode", StringComparison.OrdinalIgnoreCase), - ImageUrl = details.primaryImage, - ThumbImageUrl = details.thumbImage, - IsKids = string.Equals(details.audience, "children", StringComparison.OrdinalIgnoreCase), - IsSports = string.Equals(details.entityType, "sports", StringComparison.OrdinalIgnoreCase), + IsRepeat = programInfo.New == null, + IsSeries = string.Equals(details.EntityType, "episode", StringComparison.OrdinalIgnoreCase), + ImageUrl = details.PrimaryImage, + ThumbImageUrl = details.ThumbImage, + IsKids = string.Equals(details.Audience, "children", StringComparison.OrdinalIgnoreCase), + IsSports = string.Equals(details.EntityType, "sports", StringComparison.OrdinalIgnoreCase), IsMovie = IsMovie(details), - Etag = programInfo.md5, - IsLive = string.Equals(programInfo.liveTapeDelay, "live", StringComparison.OrdinalIgnoreCase), - IsPremiere = programInfo.premiere || (programInfo.isPremiereOrFinale ?? string.Empty).IndexOf("premiere", StringComparison.OrdinalIgnoreCase) != -1 + Etag = programInfo.Md5, + IsLive = string.Equals(programInfo.LiveTapeDelay, "live", StringComparison.OrdinalIgnoreCase), + IsPremiere = programInfo.Premiere || (programInfo.IsPremiereOrFinale ?? string.Empty).IndexOf("premiere", StringComparison.OrdinalIgnoreCase) != -1 }; var showId = programId; @@ -298,15 +299,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings info.ShowId = showId; - if (programInfo.videoProperties != null) + if (programInfo.VideoProperties != null) { - info.IsHD = programInfo.videoProperties.Contains("hdtv", StringComparer.OrdinalIgnoreCase); - info.Is3D = programInfo.videoProperties.Contains("3d", StringComparer.OrdinalIgnoreCase); + info.IsHD = programInfo.VideoProperties.Contains("hdtv", StringComparer.OrdinalIgnoreCase); + info.Is3D = programInfo.VideoProperties.Contains("3d", StringComparer.OrdinalIgnoreCase); } - if (details.contentRating != null && details.contentRating.Count > 0) + if (details.ContentRating != null && details.ContentRating.Count > 0) { - info.OfficialRating = details.contentRating[0].code.Replace("TV", "TV-", StringComparison.Ordinal) + info.OfficialRating = details.ContentRating[0].Code.Replace("TV", "TV-", StringComparison.Ordinal) .Replace("--", "-", StringComparison.Ordinal); var invalid = new[] { "N/A", "Approved", "Not Rated", "Passed" }; @@ -316,15 +317,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings } } - if (details.descriptions != null) + if (details.Descriptions != null) { - if (details.descriptions.description1000 != null && details.descriptions.description1000.Count > 0) + if (details.Descriptions.Description1000 != null && details.Descriptions.Description1000.Count > 0) { - info.Overview = details.descriptions.description1000[0].description; + info.Overview = details.Descriptions.Description1000[0].Description; } - else if (details.descriptions.description100 != null && details.descriptions.description100.Count > 0) + else if (details.Descriptions.Description100 != null && details.Descriptions.Description100.Count > 0) { - info.Overview = details.descriptions.description100[0].description; + info.Overview = details.Descriptions.Description100[0].Description; } } @@ -334,18 +335,18 @@ namespace Emby.Server.Implementations.LiveTv.Listings info.SeriesProviderIds[MetadataProvider.Zap2It.ToString()] = info.SeriesId; - if (details.metadata != null) + if (details.Metadata != null) { - foreach (var metadataProgram in details.metadata) + foreach (var metadataProgram in details.Metadata) { var gracenote = metadataProgram.Gracenote; if (gracenote != null) { - info.SeasonNumber = gracenote.season; + info.SeasonNumber = gracenote.Season; - if (gracenote.episode > 0) + if (gracenote.Episode > 0) { - info.EpisodeNumber = gracenote.episode; + info.EpisodeNumber = gracenote.Episode; } break; @@ -354,25 +355,25 @@ namespace Emby.Server.Implementations.LiveTv.Listings } } - if (!string.IsNullOrWhiteSpace(details.originalAirDate)) + if (!string.IsNullOrWhiteSpace(details.OriginalAirDate)) { - info.OriginalAirDate = DateTime.Parse(details.originalAirDate, CultureInfo.InvariantCulture); + info.OriginalAirDate = DateTime.Parse(details.OriginalAirDate, CultureInfo.InvariantCulture); info.ProductionYear = info.OriginalAirDate.Value.Year; } - if (details.movie != null) + 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; } } - if (details.genres != null) + if (details.Genres != null) { - info.Genres = details.genres.Where(g => !string.IsNullOrWhiteSpace(g)).ToList(); - info.IsNews = details.genres.Contains("news", StringComparer.OrdinalIgnoreCase); + info.Genres = details.Genres.Where(g => !string.IsNullOrWhiteSpace(g)).ToList(); + info.IsNews = details.Genres.Contains("news", StringComparer.OrdinalIgnoreCase); if (info.Genres.Contains("children", StringComparer.OrdinalIgnoreCase)) { @@ -395,11 +396,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings return date; } - private string GetProgramImage(string apiUrl, IEnumerable images, bool returnDefaultImage, double desiredAspect) + private string GetProgramImage(string apiUrl, IEnumerable images, bool returnDefaultImage, double desiredAspect) { var match = images .OrderBy(i => Math.Abs(desiredAspect - GetAspectRatio(i))) - .ThenByDescending(GetSizeOrder) + .ThenByDescending(i => GetSizeOrder(i)) .FirstOrDefault(); if (match == null) @@ -407,7 +408,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings return null; } - var uri = match.uri; + var uri = match.Uri; if (string.IsNullOrWhiteSpace(uri)) { @@ -423,19 +424,19 @@ namespace Emby.Server.Implementations.LiveTv.Listings } } - private static double GetAspectRatio(ScheduleDirect.ImageData i) + private static double GetAspectRatio(ImageDataDto i) { int width = 0; int height = 0; - if (!string.IsNullOrWhiteSpace(i.width)) + if (!string.IsNullOrWhiteSpace(i.Width)) { - int.TryParse(i.width, out width); + _ = int.TryParse(i.Width, out width); } - if (!string.IsNullOrWhiteSpace(i.height)) + if (!string.IsNullOrWhiteSpace(i.Height)) { - int.TryParse(i.height, out height); + _ = int.TryParse(i.Height, out height); } if (height == 0 || width == 0) @@ -448,14 +449,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings return result; } - private async Task> GetImageForPrograms( + private async Task> GetImageForPrograms( ListingsProviderInfo info, IReadOnlyList programIds, CancellationToken cancellationToken) { if (programIds.Count == 0) { - return new List(); + return new List(); } StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13)); @@ -479,13 +480,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings { using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false); await using var response = await innerResponse2.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - return await JsonSerializer.DeserializeAsync>(response, _jsonOptions, cancellationToken).ConfigureAwait(false); + return await JsonSerializer.DeserializeAsync>(response, _jsonOptions, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "Error getting image info from schedules direct"); - return new List(); + return new List(); } } @@ -508,18 +509,18 @@ namespace Emby.Server.Implementations.LiveTv.Listings using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false); await using var response = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var root = await JsonSerializer.DeserializeAsync>(response, _jsonOptions, cancellationToken).ConfigureAwait(false); + var root = await JsonSerializer.DeserializeAsync>(response, _jsonOptions, cancellationToken).ConfigureAwait(false); if (root != null) { - foreach (ScheduleDirect.Headends headend in root) + foreach (HeadendsDto headend in root) { - foreach (ScheduleDirect.Lineup lineup in headend.lineups) + foreach (LineupDto lineup in headend.Lineups) { lineups.Add(new NameIdPair { - Name = string.IsNullOrWhiteSpace(lineup.name) ? lineup.lineup : lineup.name, - Id = lineup.uri.Substring(18) + Name = string.IsNullOrWhiteSpace(lineup.Name) ? lineup.Lineup : lineup.Name, + Id = lineup.Uri[18..] }); } } @@ -649,14 +650,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var root = await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); - if (string.Equals(root.message, "OK", StringComparison.Ordinal)) + var root = await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); + if (string.Equals(root.Message, "OK", StringComparison.Ordinal)) { - _logger.LogInformation("Authenticated with Schedules Direct token: " + root.token); - return root.token; + _logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token); + return root.Token; } - throw new Exception("Could not authenticate with Schedules Direct Error: " + root.message); + throw new Exception("Could not authenticate with Schedules Direct Error: " + root.Message); } private async Task AddLineupToAccount(ListingsProviderInfo info, CancellationToken cancellationToken) @@ -705,9 +706,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings httpResponse.EnsureSuccessStatusCode(); await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); using var response = httpResponse.Content; - var root = await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); + var root = await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); - return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase)); + return root.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase)); } catch (HttpRequestException ex) { @@ -777,35 +778,35 @@ namespace Emby.Server.Implementations.LiveTv.Listings using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false); await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var root = await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); - _logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count); + var root = await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); + _logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.Map.Count); _logger.LogInformation("Mapping Stations to Channel"); - var allStations = root.stations ?? new List(); + var allStations = root.Stations ?? new List(); - var map = root.map; + var map = root.Map; var list = new List(map.Count); foreach (var channel in map) { var channelNumber = GetChannelNumber(channel); - var station = allStations.Find(item => string.Equals(item.stationID, channel.stationID, StringComparison.OrdinalIgnoreCase)) - ?? new ScheduleDirect.Station + var station = allStations.Find(item => string.Equals(item.StationId, channel.StationId, StringComparison.OrdinalIgnoreCase)) + ?? new StationDto { - stationID = channel.stationID + StationId = channel.StationId }; var channelInfo = new ChannelInfo { - Id = station.stationID, - CallSign = station.callsign, + Id = station.StationId, + CallSign = station.Callsign, Number = channelNumber, - Name = string.IsNullOrWhiteSpace(station.name) ? channelNumber : station.name + Name = string.IsNullOrWhiteSpace(station.Name) ? channelNumber : station.Name }; - if (station.logo != null) + if (station.Logo != null) { - channelInfo.ImageUrl = station.logo.URL; + channelInfo.ImageUrl = station.Logo.Url; } list.Add(channelInfo); @@ -818,402 +819,5 @@ namespace Emby.Server.Implementations.LiveTv.Listings { return value.Replace(" ", string.Empty, StringComparison.Ordinal).Replace("-", string.Empty, StringComparison.Ordinal); } - - public class ScheduleDirect - { - public class Token - { - public int code { get; set; } - - public string message { get; set; } - - public string serverID { get; set; } - - public string token { get; set; } - } - - public class Lineup - { - public string lineup { get; set; } - - public string name { get; set; } - - public string transport { get; set; } - - public string location { get; set; } - - public string uri { get; set; } - } - - public class Lineups - { - public int code { get; set; } - - public string serverID { get; set; } - - public string datetime { get; set; } - - public List lineups { get; set; } - } - - public class Headends - { - public string headend { get; set; } - - public string transport { get; set; } - - public string location { get; set; } - - public List lineups { get; set; } - } - - public class Map - { - public string stationID { get; set; } - - public string channel { get; set; } - - public string logicalChannelNumber { get; set; } - - public int uhfVhf { get; set; } - - public int atscMajor { get; set; } - - public int atscMinor { get; set; } - } - - public class Broadcaster - { - public string city { get; set; } - - public string state { get; set; } - - public string postalcode { get; set; } - - public string country { get; set; } - } - - public class Logo - { - public string URL { get; set; } - - public int height { get; set; } - - public int width { get; set; } - - public string md5 { get; set; } - } - - public class Station - { - public string stationID { get; set; } - - public string name { get; set; } - - public string callsign { get; set; } - - public List broadcastLanguage { get; set; } - - public List descriptionLanguage { get; set; } - - public Broadcaster broadcaster { get; set; } - - public string affiliate { get; set; } - - public Logo logo { get; set; } - - public bool? isCommercialFree { get; set; } - } - - public class Metadata - { - public string lineup { get; set; } - - public string modified { get; set; } - - public string transport { get; set; } - } - - public class Channel - { - public List map { get; set; } - - public List stations { get; set; } - - public Metadata metadata { get; set; } - } - - public class RequestScheduleForChannel - { - public string stationID { get; set; } - - public List date { get; set; } - } - - public class Rating - { - public string body { get; set; } - - public string code { get; set; } - } - - public class Multipart - { - public int partNumber { get; set; } - - public int totalParts { get; set; } - } - - public class Program - { - public string programID { get; set; } - - public string airDateTime { get; set; } - - public int duration { get; set; } - - public string md5 { get; set; } - - public List audioProperties { get; set; } - - public List videoProperties { get; set; } - - public List ratings { get; set; } - - public bool? @new { get; set; } - - public Multipart multipart { get; set; } - - public string liveTapeDelay { get; set; } - - public bool premiere { get; set; } - - public bool repeat { get; set; } - - public string isPremiereOrFinale { get; set; } - } - - public class MetadataSchedule - { - public string modified { get; set; } - - public string md5 { get; set; } - - public string startDate { get; set; } - - public string endDate { get; set; } - - public int days { get; set; } - } - - public class Day - { - public string stationID { get; set; } - - public List programs { get; set; } - - public MetadataSchedule metadata { get; set; } - - public Day() - { - programs = new List(); - } - } - - public class Title - { - public string title120 { get; set; } - } - - public class EventDetails - { - public string subType { get; set; } - } - - public class Description100 - { - public string descriptionLanguage { get; set; } - - public string description { get; set; } - } - - public class Description1000 - { - public string descriptionLanguage { get; set; } - - public string description { get; set; } - } - - public class DescriptionsProgram - { - public List description100 { get; set; } - - public List description1000 { get; set; } - } - - public class Gracenote - { - public int season { get; set; } - - public int episode { get; set; } - } - - public class MetadataPrograms - { - public Gracenote Gracenote { get; set; } - } - - public class ContentRating - { - public string body { get; set; } - - public string code { get; set; } - } - - public class Cast - { - public string billingOrder { get; set; } - - public string role { get; set; } - - public string nameId { get; set; } - - public string personId { get; set; } - - public string name { get; set; } - - public string characterName { get; set; } - } - - public class Crew - { - public string billingOrder { get; set; } - - public string role { get; set; } - - public string nameId { get; set; } - - public string personId { get; set; } - - public string name { get; set; } - } - - public class QualityRating - { - public string ratingsBody { get; set; } - - public string rating { get; set; } - - public string minRating { get; set; } - - public string maxRating { get; set; } - - public string increment { get; set; } - } - - public class Movie - { - public string year { get; set; } - - public int duration { get; set; } - - public List qualityRating { get; set; } - } - - public class Recommendation - { - public string programID { get; set; } - - public string title120 { get; set; } - } - - public class ProgramDetails - { - public string audience { get; set; } - - public string programID { get; set; } - - public List titles { get; set; } - - public EventDetails eventDetails { get; set; } - - public DescriptionsProgram descriptions { get; set; } - - public string originalAirDate { get; set; } - - public List<string> genres { get; set; } - - public string episodeTitle150 { get; set; } - - public List<MetadataPrograms> metadata { get; set; } - - public List<ContentRating> contentRating { get; set; } - - public List<Cast> cast { get; set; } - - public List<Crew> crew { get; set; } - - public string entityType { get; set; } - - public string showType { get; set; } - - public bool hasImageArtwork { get; set; } - - public string primaryImage { get; set; } - - public string thumbImage { get; set; } - - public string backdropImage { get; set; } - - public string bannerImage { get; set; } - - public string imageID { get; set; } - - public string md5 { get; set; } - - public List<string> contentAdvisory { get; set; } - - public Movie movie { get; set; } - - public List<Recommendation> recommendations { get; set; } - } - - public class Caption - { - public string content { get; set; } - - public string lang { get; set; } - } - - public class ImageData - { - public string width { get; set; } - - public string height { get; set; } - - public string uri { get; set; } - - public string size { get; set; } - - public string aspect { get; set; } - - public string category { get; set; } - - public string text { get; set; } - - public string primary { get; set; } - - public string tier { get; set; } - - public Caption caption { get; set; } - } - - public class ShowImages - { - public string programID { get; set; } - - public List<ImageData> data { get; set; } - } - } } } diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs new file mode 100644 index 0000000000..b881b307c3 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs @@ -0,0 +1,36 @@ +#nullable disable + +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Broadcaster dto. + /// </summary> + public class BroadcasterDto + { + /// <summary> + /// Gets or sets the city. + /// </summary> + [JsonPropertyName("city")] + public string City { get; set; } + + /// <summary> + /// Gets or sets the state. + /// </summary> + [JsonPropertyName("state")] + public string State { get; set; } + + /// <summary> + /// Gets or sets the postal code. + /// </summary> + [JsonPropertyName("postalCode")] + public string Postalcode { get; set; } + + /// <summary> + /// Gets or sets the country. + /// </summary> + [JsonPropertyName("country")] + public string Country { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs new file mode 100644 index 0000000000..96b67d1eb3 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs @@ -0,0 +1,24 @@ +#nullable disable + +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Caption dto. + /// </summary> + public class CaptionDto + { + /// <summary> + /// Gets or sets the content. + /// </summary> + [JsonPropertyName("content")] + public string Content { get; set; } + + /// <summary> + /// Gets or sets the lang. + /// </summary> + [JsonPropertyName("lang")] + public string Lang { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs new file mode 100644 index 0000000000..dac6f5f3e0 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs @@ -0,0 +1,48 @@ +#nullable disable + +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Cast dto. + /// </summary> + public class CastDto + { + /// <summary> + /// Gets or sets the billing order. + /// </summary> + [JsonPropertyName("billingOrder")] + public string BillingOrder { get; set; } + + /// <summary> + /// Gets or sets the role. + /// </summary> + [JsonPropertyName("role")] + public string Role { get; set; } + + /// <summary> + /// Gets or sets the name id. + /// </summary> + [JsonPropertyName("nameId")] + public string NameId { get; set; } + + /// <summary> + /// Gets or sets the person id. + /// </summary> + [JsonPropertyName("personId")] + public string PersonId { get; set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + [JsonPropertyName("name")] + public string Name { get; set; } + + /// <summary> + /// Gets or sets the character name. + /// </summary> + [JsonPropertyName("characterName")] + public string CharacterName { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs new file mode 100644 index 0000000000..8c9c2c1fcc --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs @@ -0,0 +1,31 @@ +#nullable disable + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Channel dto. + /// </summary> + public class ChannelDto + { + /// <summary> + /// Gets or sets the list of maps. + /// </summary> + [JsonPropertyName("map")] + public List<MapDto> Map { get; set; } + + /// <summary> + /// Gets or sets the list of stations. + /// </summary> + [JsonPropertyName("stations")] + public List<StationDto> Stations { get; set; } + + /// <summary> + /// Gets or sets the metadata. + /// </summary> + [JsonPropertyName("metadata")] + public MetadataDto Metadata { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs new file mode 100644 index 0000000000..135b5bb08b --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs @@ -0,0 +1,24 @@ +#nullable disable + +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Content rating dto. + /// </summary> + public class ContentRatingDto + { + /// <summary> + /// Gets or sets the body. + /// </summary> + [JsonPropertyName("body")] + public string Body { get; set; } + + /// <summary> + /// Gets or sets the code. + /// </summary> + [JsonPropertyName("code")] + public string Code { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs new file mode 100644 index 0000000000..82d1001c88 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs @@ -0,0 +1,42 @@ +#nullable disable + +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Crew dto. + /// </summary> + public class CrewDto + { + /// <summary> + /// Gets or sets the billing order. + /// </summary> + [JsonPropertyName("billingOrder")] + public string BillingOrder { get; set; } + + /// <summary> + /// Gets or sets the role. + /// </summary> + [JsonPropertyName("role")] + public string Role { get; set; } + + /// <summary> + /// Gets or sets the name id. + /// </summary> + [JsonPropertyName("nameId")] + public string NameId { get; set; } + + /// <summary> + /// Gets or sets the person id. + /// </summary> + [JsonPropertyName("personId")] + public string PersonId { get; set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + [JsonPropertyName("name")] + public string Name { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs new file mode 100644 index 0000000000..68876b0686 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs @@ -0,0 +1,39 @@ +#nullable disable + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Day dto. + /// </summary> + public class DayDto + { + /// <summary> + /// Initializes a new instance of the <see cref="DayDto"/> class. + /// </summary> + public DayDto() + { + Programs = new List<ProgramDto>(); + } + + /// <summary> + /// Gets or sets the station id. + /// </summary> + [JsonPropertyName("stationID")] + public string StationId { get; set; } + + /// <summary> + /// Gets or sets the list of programs. + /// </summary> + [JsonPropertyName("programs")] + public List<ProgramDto> Programs { get; set; } + + /// <summary> + /// Gets or sets the metadata schedule. + /// </summary> + [JsonPropertyName("metadata")] + public MetadataScheduleDto Metadata { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs new file mode 100644 index 0000000000..d3e6ff3935 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs @@ -0,0 +1,24 @@ +#nullable disable + +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Description 1_000 dto. + /// </summary> + public class Description1000Dto + { + /// <summary> + /// Gets or sets the description language. + /// </summary> + [JsonPropertyName("descriptionLanguage")] + public string DescriptionLanguage { get; set; } + + /// <summary> + /// Gets or sets the description. + /// </summary> + [JsonPropertyName("description")] + public string Description { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs new file mode 100644 index 0000000000..04360266c9 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs @@ -0,0 +1,24 @@ +#nullable disable + +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Description 100 dto. + /// </summary> + public class Description100Dto + { + /// <summary> + /// Gets or sets the description language. + /// </summary> + [JsonPropertyName("descriptionLanguage")] + public string DescriptionLanguage { get; set; } + + /// <summary> + /// Gets or sets the description. + /// </summary> + [JsonPropertyName("description")] + public string Description { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs new file mode 100644 index 0000000000..3af36ae965 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs @@ -0,0 +1,25 @@ +#nullable disable + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Descriptions program dto. + /// </summary> + public class DescriptionsProgramDto + { + /// <summary> + /// Gets or sets the list of description 100. + /// </summary> + [JsonPropertyName("description100")] + public List<Description100Dto> Description100 { get; set; } + + /// <summary> + /// Gets or sets the list of description1000. + /// </summary> + [JsonPropertyName("description1000")] + public List<Description1000Dto> Description1000 { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs new file mode 100644 index 0000000000..c3b2bd9c13 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs @@ -0,0 +1,18 @@ +#nullable disable + +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Event details dto. + /// </summary> + public class EventDetailsDto + { + /// <summary> + /// Gets or sets the sub type. + /// </summary> + [JsonPropertyName("subType")] + public string SubType { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs new file mode 100644 index 0000000000..3d8bea362a --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs @@ -0,0 +1,24 @@ +#nullable disable + +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Gracenote dto. + /// </summary> + public class GracenoteDto + { + /// <summary> + /// Gets or sets the season. + /// </summary> + [JsonPropertyName("season")] + public int Season { get; set; } + + /// <summary> + /// Gets or sets the episode. + /// </summary> + [JsonPropertyName("episode")] + public int Episode { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs new file mode 100644 index 0000000000..1fb3decb23 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs @@ -0,0 +1,37 @@ +#nullable disable + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Headends dto. + /// </summary> + public class HeadendsDto + { + /// <summary> + /// Gets or sets the headend. + /// </summary> + [JsonPropertyName("headend")] + public string Headend { get; set; } + + /// <summary> + /// Gets or sets the transport. + /// </summary> + [JsonPropertyName("transport")] + public string Transport { get; set; } + + /// <summary> + /// Gets or sets the location. + /// </summary> + [JsonPropertyName("location")] + public string Location { get; set; } + + /// <summary> + /// Gets or sets the list of lineups. + /// </summary> + [JsonPropertyName("lineups")] + public List<LineupDto> Lineups { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs new file mode 100644 index 0000000000..b80ac624cc --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs @@ -0,0 +1,69 @@ +#nullable disable + +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + public class ImageDataDto + { + /// <summary> + /// Gets or sets the width. + /// </summary> + [JsonPropertyName("width")] + public string Width { get; set; } + + /// <summary> + /// Gets or sets the height. + /// </summary> + [JsonPropertyName("height")] + public string Height { get; set; } + + /// <summary> + /// Gets or sets the uri. + /// </summary> + [JsonPropertyName("uri")] + public string Uri { get; set; } + + /// <summary> + /// Gets or sets the size. + /// </summary> + [JsonPropertyName("size")] + public string Size { get; set; } + + /// <summary> + /// Gets or sets the aspect. + /// </summary> + [JsonPropertyName("aspect")] + public string aspect { get; set; } + + /// <summary> + /// Gets or sets the category. + /// </summary> + [JsonPropertyName("category")] + public string Category { get; set; } + + /// <summary> + /// Gets or sets the text. + /// </summary> + [JsonPropertyName("text")] + public string Text { get; set; } + + /// <summary> + /// Gets or sets the primary. + /// </summary> + [JsonPropertyName("primary")] + public string Primary { get; set; } + + /// <summary> + /// Gets or sets the tier. + /// </summary> + [JsonPropertyName("tier")] + public string Tier { get; set; } + + /// <summary> + /// Gets or sets the caption. + /// </summary> + [JsonPropertyName("caption")] + public CaptionDto Caption { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs new file mode 100644 index 0000000000..b8f27a8ac7 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs @@ -0,0 +1,42 @@ +#nullable disable + +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// The lineup dto. + /// </summary> + public class LineupDto + { + /// <summary> + /// Gets or sets the linup. + /// </summary> + [JsonPropertyName("lineup")] + public string Lineup { get; set; } + + /// <summary> + /// Gets or sets the lineup name. + /// </summary> + [JsonPropertyName("name")] + public string Name { get; set; } + + /// <summary> + /// Gets or sets the transport. + /// </summary> + [JsonPropertyName("transport.")] + public string Transport { get; set; } + + /// <summary> + /// Gets or sets the location. + /// </summary> + [JsonPropertyName("location")] + public string Location { get; set; } + + /// <summary> + /// Gets or sets the uri. + /// </summary> + [JsonPropertyName("uri")] + public string Uri { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs new file mode 100644 index 0000000000..15139ba3b2 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs @@ -0,0 +1,37 @@ +#nullable disable + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Lineups dto. + /// </summary> + public class LineupsDto + { + /// <summary> + /// Gets or sets the response code. + /// </summary> + [JsonPropertyName("code")] + public int Code { get; set; } + + /// <summary> + /// Gets or sets the server id. + /// </summary> + [JsonPropertyName("serverID")] + public string ServerId { get; set; } + + /// <summary> + /// Gets or sets the datetime. + /// </summary> + [JsonPropertyName("datetime")] + public string Datetime { get; set; } + + /// <summary> + /// Gets or sets the list of lineups. + /// </summary> + [JsonPropertyName("lineups")] + public List<LineupDto> Lineups { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs new file mode 100644 index 0000000000..7b235ed7f6 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs @@ -0,0 +1,36 @@ +#nullable disable + +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Logo dto. + /// </summary> + public class LogoDto + { + /// <summary> + /// Gets or sets the url. + /// </summary> + [JsonPropertyName("URL")] + public string Url { get; set; } + + /// <summary> + /// Gets or sets the height. + /// </summary> + [JsonPropertyName("height")] + public int Height { get; set; } + + /// <summary> + /// Gets or sets the width. + /// </summary> + [JsonPropertyName("width")] + public int Width { get; set; } + + /// <summary> + /// Gets or sets the md5. + /// </summary> + [JsonPropertyName("md5")] + public string Md5 { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs new file mode 100644 index 0000000000..8d45e8fff7 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs @@ -0,0 +1,48 @@ +#nullable disable + +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Map dto. + /// </summary> + public class MapDto + { + /// <summary> + /// Gets or sets the station id. + /// </summary> + [JsonPropertyName("stationID")] + public string StationId { get; set; } + + /// <summary> + /// Gets or sets the channel. + /// </summary> + [JsonPropertyName("channel")] + public string Channel { get; set; } + + /// <summary> + /// Gets or sets the logical channel number. + /// </summary> + [JsonPropertyName("logicalChannelNumber")] + public string LogicalChannelNumber { get; set; } + + /// <summary> + /// Gets or sets the uhfvhf. + /// </summary> + [JsonPropertyName("uhfVhf")] + public int UhfVhf { get; set; } + + /// <summary> + /// Gets or sets the astc major. + /// </summary> + [JsonPropertyName("astcMajor")] + public int AtscMajor { get; set; } + + /// <summary> + /// Gets or sets the astc minor. + /// </summary> + [JsonPropertyName("astcMinor")] + public int AtscMinor { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs new file mode 100644 index 0000000000..5a3893a35f --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs @@ -0,0 +1,30 @@ +#nullable disable + +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Metadata dto. + /// </summary> + public class MetadataDto + { + /// <summary> + /// Gets or sets the linup. + /// </summary> + [JsonPropertyName("lineup")] + public string Lineup { get; set; } + + /// <summary> + /// Gets or sets the modified timestamp. + /// </summary> + [JsonPropertyName("modified")] + public string Modified { get; set; } + + /// <summary> + /// Gets or sets the transport. + /// </summary> + [JsonPropertyName("transport")] + public string Transport { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs new file mode 100644 index 0000000000..4057e98024 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs @@ -0,0 +1,18 @@ +#nullable disable + +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Metadata programs dto. + /// </summary> + public class MetadataProgramsDto + { + /// <summary> + /// Gets or sets the gracenote object. + /// </summary> + [JsonPropertyName("gracenote")] + public GracenoteDto Gracenote { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs new file mode 100644 index 0000000000..4979296da4 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs @@ -0,0 +1,42 @@ +#nullable disable + +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Metadata schedule dto. + /// </summary> + public class MetadataScheduleDto + { + /// <summary> + /// Gets or sets the modified timestamp. + /// </summary> + [JsonPropertyName("modified")] + public string Modified { get; set; } + + /// <summary> + /// Gets or sets the md5. + /// </summary> + [JsonPropertyName("md5")] + public string Md5 { get; set; } + + /// <summary> + /// Gets or sets the start date. + /// </summary> + [JsonPropertyName("startDate")] + public string StartDate { get; set; } + + /// <summary> + /// Gets or sets the end date. + /// </summary> + [JsonPropertyName("endDate")] + public string EndDate { get; set; } + + /// <summary> + /// Gets or sets the days count. + /// </summary> + [JsonPropertyName("days")] + public int Days { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs new file mode 100644 index 0000000000..48d731d890 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs @@ -0,0 +1,31 @@ +#nullable disable + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Movie dto. + /// </summary> + public class MovieDto + { + /// <summary> + /// Gets or sets the year. + /// </summary> + [JsonPropertyName("year")] + public string Year { get; set; } + + /// <summary> + /// Gets or sets the duration. + /// </summary> + [JsonPropertyName("duration")] + public int Duration { get; set; } + + /// <summary> + /// Gets or sets the list of quality rating. + /// </summary> + [JsonPropertyName("qualityRating")] + public List<QualityRatingDto> QualityRating { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs new file mode 100644 index 0000000000..42eddfff26 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs @@ -0,0 +1,24 @@ +#nullable disable + +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Multipart dto. + /// </summary> + public class MultipartDto + { + /// <summary> + /// Gets or sets the part number. + /// </summary> + [JsonPropertyName("partNumber")] + public int PartNumber { get; set; } + + /// <summary> + /// Gets or sets the total parts. + /// </summary> + [JsonPropertyName("totalParts")] + public int TotalParts { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs new file mode 100644 index 0000000000..a84c47c129 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs @@ -0,0 +1,157 @@ +#nullable disable + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Program details dto. + /// </summary> + public class ProgramDetailsDto + { + /// <summary> + /// Gets or sets the audience. + /// </summary> + [JsonPropertyName("audience")] + public string Audience { get; set; } + + /// <summary> + /// Gets or sets the program id. + /// </summary> + [JsonPropertyName("programID")] + public string ProgramId { get; set; } + + /// <summary> + /// Gets or sets the list of titles. + /// </summary> + [JsonPropertyName("titles")] + public List<TitleDto> Titles { get; set; } + + /// <summary> + /// Gets or sets the event details object. + /// </summary> + [JsonPropertyName("eventDetails")] + public EventDetailsDto EventDetails { get; set; } + + /// <summary> + /// Gets or sets the descriptions. + /// </summary> + [JsonPropertyName("descriptions")] + public DescriptionsProgramDto Descriptions { get; set; } + + /// <summary> + /// Gets or sets the original air date. + /// </summary> + [JsonPropertyName("originalAirDate")] + public string OriginalAirDate { get; set; } + + /// <summary> + /// Gets or sets the list of genres. + /// </summary> + [JsonPropertyName("genres")] + public List<string> Genres { get; set; } + + /// <summary> + /// Gets or sets the episode title. + /// </summary> + [JsonPropertyName("episodeTitle150")] + public string EpisodeTitle150 { get; set; } + + /// <summary> + /// Gets or sets the list of metadata. + /// </summary> + [JsonPropertyName("metadata")] + public List<MetadataProgramsDto> Metadata { get; set; } + + /// <summary> + /// Gets or sets the list of content raitings. + /// </summary> + [JsonPropertyName("contentRating")] + public List<ContentRatingDto> ContentRating { get; set; } + + /// <summary> + /// Gets or sets the list of cast. + /// </summary> + [JsonPropertyName("cast")] + public List<CastDto> Cast { get; set; } + + /// <summary> + /// Gets or sets the list of crew. + /// </summary> + [JsonPropertyName("crew")] + public List<CrewDto> Crew { get; set; } + + /// <summary> + /// Gets or sets the entity type. + /// </summary> + [JsonPropertyName("entityType")] + public string EntityType { get; set; } + + /// <summary> + /// Gets or sets the show type. + /// </summary> + [JsonPropertyName("showType")] + public string ShowType { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether there is image artwork. + /// </summary> + [JsonPropertyName("hasImageArtwork")] + public bool HasImageArtwork { get; set; } + + /// <summary> + /// Gets or sets the primary image. + /// </summary> + [JsonPropertyName("primaryImage")] + public string PrimaryImage { get; set; } + + /// <summary> + /// Gets or sets the thumb image. + /// </summary> + [JsonPropertyName("thumbImage")] + public string ThumbImage { get; set; } + + /// <summary> + /// Gets or sets the backdrop image. + /// </summary> + [JsonPropertyName("backdropImage")] + public string BackdropImage { get; set; } + + /// <summary> + /// Gets or sets the banner image. + /// </summary> + [JsonPropertyName("bannerImage")] + public string BannerImage { get; set; } + + /// <summary> + /// Gets or sets the image id. + /// </summary> + [JsonPropertyName("imageID")] + public string ImageId { get; set; } + + /// <summary> + /// Gets or sets the md5. + /// </summary> + [JsonPropertyName("md5")] + public string Md5 { get; set; } + + /// <summary> + /// Gets or sets the list of content advisory. + /// </summary> + [JsonPropertyName("contentAdvisory")] + public List<string> ContentAdvisory { get; set; } + + /// <summary> + /// Gets or sets the movie object. + /// </summary> + [JsonPropertyName("movie")] + public MovieDto Movie { get; set; } + + /// <summary> + /// Gets or sets the list of recommendations. + /// </summary> + [JsonPropertyName("recommendations")] + public List<RecommendationDto> Recommendations { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs new file mode 100644 index 0000000000..ad5389100c --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs @@ -0,0 +1,91 @@ +#nullable disable + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Program dto. + /// </summary> + public class ProgramDto + { + /// <summary> + /// Gets or sets the program id. + /// </summary> + [JsonPropertyName("programID")] + public string ProgramId { get; set; } + + /// <summary> + /// Gets or sets the air date time. + /// </summary> + [JsonPropertyName("airDateTime")] + public string AirDateTime { get; set; } + + /// <summary> + /// Gets or sets the duration. + /// </summary> + [JsonPropertyName("duration")] + public int Duration { get; set; } + + /// <summary> + /// Gets or sets the md5. + /// </summary> + [JsonPropertyName("md5")] + public string Md5 { get; set; } + + /// <summary> + /// Gets or sets the list of audio properties. + /// </summary> + [JsonPropertyName("audioProperties")] + public List<string> AudioProperties { get; set; } + + /// <summary> + /// Gets or sets the list of video properties. + /// </summary> + [JsonPropertyName("videoProperties")] + public List<string> VideoProperties { get; set; } + + /// <summary> + /// Gets or sets the list of ratings. + /// </summary> + [JsonPropertyName("ratings")] + public List<RatingDto> Ratings { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this program is new. + /// </summary> + [JsonPropertyName("new")] + public bool? New { get; set; } + + /// <summary> + /// Gets or sets the multipart object. + /// </summary> + [JsonPropertyName("multipart")] + public MultipartDto Multipart { get; set; } + + /// <summary> + /// Gets or sets the live tape delay. + /// </summary> + [JsonPropertyName("liveTapeDelay")] + public string LiveTapeDelay { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this is the premiere. + /// </summary> + [JsonPropertyName("premiere")] + public bool Premiere { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this is a repeat. + /// </summary> + [JsonPropertyName("repeat")] + public bool Repeat { get; set; } + + /// <summary> + /// Gets or sets the premiere or finale. + /// </summary> + [JsonPropertyName("isPremiereOrFinale")] + public string IsPremiereOrFinale { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs new file mode 100644 index 0000000000..5cd0a74595 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs @@ -0,0 +1,42 @@ +#nullable disable + +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Quality rating dto. + /// </summary> + public class QualityRatingDto + { + /// <summary> + /// Gets or sets the ratings body. + /// </summary> + [JsonPropertyName("ratingsBody")] + public string RatingsBody { get; set; } + + /// <summary> + /// Gets or sets the rating. + /// </summary> + [JsonPropertyName("rating")] + public string Rating { get; set; } + + /// <summary> + /// Gets or sets the min rating. + /// </summary> + [JsonPropertyName("minRating")] + public string MinRating { get; set; } + + /// <summary> + /// Gets or sets the max rating. + /// </summary> + [JsonPropertyName("maxRating")] + public string MaxRating { get; set; } + + /// <summary> + /// Gets or sets the increment. + /// </summary> + [JsonPropertyName("increment")] + public string Increment { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs new file mode 100644 index 0000000000..948b831446 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs @@ -0,0 +1,24 @@ +#nullable disable + +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Rating dto. + /// </summary> + public class RatingDto + { + /// <summary> + /// Gets or sets the body. + /// </summary> + [JsonPropertyName("body")] + public string Body { get; set; } + + /// <summary> + /// Gets or sets the code. + /// </summary> + [JsonPropertyName("code")] + public string Code { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs new file mode 100644 index 0000000000..1308f45ce7 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs @@ -0,0 +1,24 @@ +#nullable disable + +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Recommendation dto. + /// </summary> + public class RecommendationDto + { + /// <summary> + /// Gets or sets the program id. + /// </summary> + [JsonPropertyName("programID")] + public string ProgramId { get; set; } + + /// <summary> + /// Gets or sets the title. + /// </summary> + [JsonPropertyName("title120")] + public string Title120 { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs new file mode 100644 index 0000000000..fb7a31ac85 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs @@ -0,0 +1,25 @@ +#nullable disable + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Request schedule for channel dto. + /// </summary> + public class RequestScheduleForChannelDto + { + /// <summary> + /// Gets or sets the station id. + /// </summary> + [JsonPropertyName("stationID")] + public string StationId { get; set; } + + /// <summary> + /// Gets or sets the list of dates. + /// </summary> + [JsonPropertyName("date")] + public List<string> Date { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs new file mode 100644 index 0000000000..332edf7c38 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs @@ -0,0 +1,22 @@ +#nullable disable + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + public class ShowImagesDto + { + /// <summary> + /// Gets or sets the program id. + /// </summary> + [JsonPropertyName("programID")] + public string ProgramId { get; set; } + + /// <summary> + /// Gets or sets the list of data. + /// </summary> + [JsonPropertyName("data")] + public List<ImageDataDto> Data { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs new file mode 100644 index 0000000000..12f3576c65 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs @@ -0,0 +1,67 @@ +#nullable disable + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Station dto. + /// </summary> + public class StationDto + { + /// <summary> + /// Gets or sets the station id. + /// </summary> + [JsonPropertyName("stationID")] + public string StationId { get; set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + [JsonPropertyName("name")] + public string Name { get; set; } + + /// <summary> + /// Gets or sets the callsign. + /// </summary> + [JsonPropertyName("callsign")] + public string Callsign { get; set; } + + /// <summary> + /// Gets or sets the broadcast language. + /// </summary> + [JsonPropertyName("broadcastLanguage")] + public List<string> BroadcastLanguage { get; set; } + + /// <summary> + /// Gets or sets the description language. + /// </summary> + [JsonPropertyName("descriptionLanguage")] + public List<string> DescriptionLanguage { get; set; } + + /// <summary> + /// Gets or sets the broadcaster. + /// </summary> + [JsonPropertyName("broadcaster")] + public BroadcasterDto Broadcaster { get; set; } + + /// <summary> + /// Gets or sets the affiliate. + /// </summary> + [JsonPropertyName("affiliate")] + public string Affiliate { get; set; } + + /// <summary> + /// Gets or sets the logo. + /// </summary> + [JsonPropertyName("logo")] + public LogoDto Logo { get; set; } + + /// <summary> + /// Gets or set a value indicating whether it is commercial free. + /// </summary> + [JsonPropertyName("isCommercialFree")] + public bool? IsCommercialFree { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs new file mode 100644 index 0000000000..06c95524b0 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs @@ -0,0 +1,18 @@ +#nullable disable + +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// Title dto. + /// </summary> + public class TitleDto + { + /// <summary> + /// Gets or sets the title. + /// </summary> + [JsonPropertyName("title120")] + public string Title120 { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs new file mode 100644 index 0000000000..c3ec1c7d61 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs @@ -0,0 +1,36 @@ +#nullable disable + +using System.Text.Json.Serialization; + +namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +{ + /// <summary> + /// The token dto. + /// </summary> + public class TokenDto + { + /// <summary> + /// Gets or sets the response code. + /// </summary> + [JsonPropertyName("code")] + public int Code { get; set; } + + /// <summary> + /// Gets or sets the response message. + /// </summary> + [JsonPropertyName("message")] + public string Message { get; set; } + + /// <summary> + /// Gets or sets the server id. + /// </summary> + [JsonPropertyName("serverID")] + public string ServerId { get; set; } + + /// <summary> + /// Gets or sets the token. + /// </summary> + [JsonPropertyName("token")] + public string Token { get; set; } + } +} diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index d964769b59..ce585d0fb5 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -403,7 +403,7 @@ namespace Emby.Server.Implementations.LiveTv // Set the total bitrate if not already supplied mediaSource.InferTotalBitrate(); - if (!(service is EmbyTV.EmbyTV)) + if (service is not EmbyTV.EmbyTV) { // We can't trust that we'll be able to direct stream it through emby server, no matter what the provider says // mediaSource.SupportsDirectPlay = false; @@ -1724,7 +1724,7 @@ namespace Emby.Server.Implementations.LiveTv await service.CancelTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false); - if (!(service is EmbyTV.EmbyTV)) + if (service is not EmbyTV.EmbyTV) { TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>(new TimerEventInfo(id))); } @@ -2050,7 +2050,7 @@ namespace Emby.Server.Implementations.LiveTv _logger.LogInformation("New recording scheduled"); - if (!(service is EmbyTV.EmbyTV)) + if (service is not EmbyTV.EmbyTV) { TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>( new TimerEventInfo(newTimerId) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index 3016eeda24..b2e555c7dc 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -27,6 +27,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { private string _channel; private string _program; + public LegacyHdHomerunChannelCommands(string url) { // parse url for channel and program diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 8cafde38ee..b07798fa40 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -260,7 +260,7 @@ namespace Emby.Server.Implementations.Playlists public async Task RemoveFromPlaylistAsync(string playlistId, IEnumerable<string> entryIds) { - if (!(_libraryManager.GetItemById(playlistId) is Playlist playlist)) + if (_libraryManager.GetItemById(playlistId) is not Playlist playlist) { throw new ArgumentException("No Playlist exists with the supplied Id"); } @@ -293,7 +293,7 @@ namespace Emby.Server.Implementations.Playlists public async Task MoveItemAsync(string playlistId, string entryId, int newIndex) { - if (!(_libraryManager.GetItemById(playlistId) is Playlist playlist)) + if (_libraryManager.GetItemById(playlistId) is not Playlist playlist) { throw new ArgumentException("No Playlist exists with the supplied Id"); } diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index b343250419..fb93c375d8 100644 --- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -24,7 +24,6 @@ namespace Emby.Server.Implementations.ScheduledTasks /// </summary> public class ScheduledTaskWorker : IScheduledTaskWorker { - /// <summary> /// Gets or sets the application paths. /// </summary> @@ -267,7 +266,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// <summary> - /// Gets the triggers that define when the task will run. + /// Gets or sets the triggers that define when the task will run. /// </summary> /// <value>The triggers.</value> /// <exception cref="ArgumentNullException"><c>value</c> is <c>null</c>.</exception> diff --git a/Emby.Server.Implementations/Sorting/ArtistComparer.cs b/Emby.Server.Implementations/Sorting/ArtistComparer.cs index 98bee3fd9f..7b7ba57531 100644 --- a/Emby.Server.Implementations/Sorting/ArtistComparer.cs +++ b/Emby.Server.Implementations/Sorting/ArtistComparer.cs @@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.Sorting /// <returns>System.String.</returns> private static string? GetValue(BaseItem? x) { - if (!(x is Audio audio)) + if (x is not Audio audio) { return string.Empty; } diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index a9f4a5a58c..64d7b2f3e0 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -154,11 +154,11 @@ namespace Jellyfin.Api.Controllers }; if (!item.IsVirtualItem - && !(item is ICollectionFolder) - && !(item is UserView) - && !(item is AggregateFolder) - && !(item is LiveTvChannel) - && !(item is IItemByName) + && item is not ICollectionFolder + && item is not UserView + && item is not AggregateFolder + && item is not LiveTvChannel + && item is not IItemByName && item.SourceType == SourceType.Library) { var inheritedContentType = _libraryManager.GetInheritedContentType(item); diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 35c27dd0e9..52eefc5c23 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -241,7 +241,7 @@ namespace Jellyfin.Api.Controllers var item = _libraryManager.GetParentItem(parentId, userId); QueryResult<BaseItem> result; - if (!(item is Folder folder)) + if (item is not Folder folder) { folder = _libraryManager.GetUserRootFolder(); } @@ -285,7 +285,7 @@ namespace Jellyfin.Api.Controllers return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}."); } - if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || !(item is UserRootFolder)) + if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || item is not UserRootFolder) { var query = new InternalItemsQuery(user!) { diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 4ed15e1d51..8cc369a5cd 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -700,7 +700,7 @@ namespace Jellyfin.Api.Controllers : _libraryManager.RootFolder) : _libraryManager.GetItemById(itemId); - if (item is Episode || (item is IItemByName && !(item is MusicArtist))) + if (item is Episode || (item is IItemByName && item is not MusicArtist)) { return new QueryResult<BaseItemDto>(); } diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 51d40994e1..7c5b8a43b7 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -228,7 +228,7 @@ namespace Jellyfin.Api.Controllers if (seasonId.HasValue) // Season id was supplied. Get episodes by season id. { var item = _libraryManager.GetItemById(seasonId.Value); - if (!(item is Season seasonItem)) + if (item is not Season seasonItem) { return NotFound("No season exists with Id " + seasonId); } @@ -237,7 +237,7 @@ namespace Jellyfin.Api.Controllers } else if (season.HasValue) // Season number was supplied. Get episodes by season number { - if (!(_libraryManager.GetItemById(seriesId) is Series series)) + if (_libraryManager.GetItemById(seriesId) is not Series series) { return NotFound("Series not found"); } @@ -252,7 +252,7 @@ namespace Jellyfin.Api.Controllers } else // No season number or season id was supplied. Returning all episodes. { - if (!(_libraryManager.GetItemById(seriesId) is Series series)) + if (_libraryManager.GetItemById(seriesId) is not Series series) { return NotFound("Series not found"); } @@ -336,7 +336,7 @@ namespace Jellyfin.Api.Controllers ? _userManager.GetUserById(userId.Value) : null; - if (!(_libraryManager.GetItemById(seriesId) is Series series)) + if (_libraryManager.GetItemById(seriesId) is not Series series) { return NotFound("Series not found"); } diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 8cffe9c4c9..5b14b6468b 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -222,11 +222,7 @@ namespace Jellyfin.Api.Helpers { var resolution = ResolutionNormalizer.Normalize( state.VideoStream?.BitRate, - state.VideoStream?.Width, - state.VideoStream?.Height, state.OutputVideoBitrate.Value, - state.VideoStream?.Codec, - state.OutputVideoCodec, state.VideoRequest.MaxWidth, state.VideoRequest.MaxHeight); diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 067fecd878..3b182f7c98 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -112,7 +112,7 @@ namespace MediaBrowser.Controller.Entities private string _name; - public static char SlugChar = '-'; + public const char SlugChar = '-'; protected BaseItem() { @@ -2050,7 +2050,7 @@ namespace MediaBrowser.Controller.Entities public virtual string GetClientTypeName() { - if (IsFolder && SourceType == SourceType.Channel && !(this is Channel)) + if (IsFolder && SourceType == SourceType.Channel && this is not Channel) { return "ChannelFolderItem"; } diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index d45a02cf2d..dd08c31eda 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -233,7 +233,7 @@ namespace MediaBrowser.Controller.Entities public override bool IsVisible(User user) { - if (this is ICollectionFolder && !(this is BasePluginFolder)) + if (this is ICollectionFolder && this is not BasePluginFolder) { var blockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders); if (blockedMediaFolders.Length > 0) @@ -673,7 +673,7 @@ namespace MediaBrowser.Controller.Entities { if (LinkedChildren.Length > 0) { - if (!(this is ICollectionFolder)) + if (this is not ICollectionFolder) { return GetChildren(user, true).Count; } @@ -730,7 +730,7 @@ namespace MediaBrowser.Controller.Entities return PostFilterAndSort(items, query, true); } - if (!(this is UserRootFolder) && !(this is AggregateFolder) && query.ParentId == Guid.Empty) + if (this is not UserRootFolder && this is not AggregateFolder && query.ParentId == Guid.Empty) { query.Parent = this; } @@ -805,7 +805,7 @@ namespace MediaBrowser.Controller.Entities { if (LinkedChildren.Length > 0) { - if (!(this is ICollectionFolder)) + if (this is not ICollectionFolder) { Logger.LogDebug("Query requires post-filtering due to LinkedChildren. Type: " + GetType().Name); return true; @@ -1545,7 +1545,7 @@ namespace MediaBrowser.Controller.Entities var childOwner = child.GetOwner() ?? child; - if (childOwner != null && !(child is IItemByName)) + if (child is not IItemByName) { var childProtocol = childOwner.PathProtocol; if (!childProtocol.HasValue || childProtocol.Value != Model.MediaInfo.MediaProtocol.File) diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index beda504b91..e4933e9682 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -296,7 +296,7 @@ namespace MediaBrowser.Controller.Entities.TV // Refresh seasons foreach (var item in items) { - if (!(item is Season)) + if (item is not Season) { continue; } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 141bb91c57..3e577ebb76 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -504,7 +504,9 @@ namespace MediaBrowser.Controller.MediaEncoding var arg = new StringBuilder(); var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty; var outputVideoCodec = GetVideoEncoder(state, encodingOptions) ?? string.Empty; +#pragma warning disable CA1508 // Defaults to string.Empty var isSwDecoder = string.IsNullOrEmpty(videoDecoder); +#pragma warning restore CA1508 var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1; var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; var isVaapiEncoder = outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; @@ -1759,7 +1761,7 @@ namespace MediaBrowser.Controller.MediaEncoding var request = state.BaseRequest; - var inputChannels = audioStream?.Channels; + var inputChannels = audioStream.Channels; if (inputChannels <= 0) { @@ -2027,8 +2029,8 @@ namespace MediaBrowser.Controller.MediaEncoding { // Adjust the size of graphical subtitles to fit the video stream. var videoStream = state.VideoStream; - var inputWidth = videoStream?.Width; - var inputHeight = videoStream?.Height; + var inputWidth = videoStream.Width; + var inputHeight = videoStream.Height; var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight); if (width.HasValue && height.HasValue) @@ -3101,7 +3103,7 @@ namespace MediaBrowser.Controller.MediaEncoding inputModifier += " " + videoDecoder; if (!IsCopyCodec(state.OutputVideoCodec) - && (videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1) + && videoDecoder.IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1) { var videoStream = state.VideoStream; var inputWidth = videoStream?.Width; @@ -3110,7 +3112,7 @@ namespace MediaBrowser.Controller.MediaEncoding var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight); - if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1 + if (videoDecoder.IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1 && width.HasValue && height.HasValue) { diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index fa9f40d60d..b09b7dba6c 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -422,7 +422,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (EncodingHelper.IsCopyCodec(OutputVideoCodec)) { - return VideoStream?.Codec; + return VideoStream.Codec; } return OutputVideoCodec; @@ -440,7 +440,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (EncodingHelper.IsCopyCodec(OutputAudioCodec)) { - return AudioStream?.Codec; + return AudioStream.Codec; } return OutputAudioCodec; @@ -568,7 +568,7 @@ namespace MediaBrowser.Controller.MediaEncoding } } - return forceDeinterlaceIfSourceIsInterlaced && isInputInterlaced; + return forceDeinterlaceIfSourceIsInterlaced; } public string[] GetRequestedProfiles(string codec) diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs index 31475e22f4..b7398880ef 100644 --- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs @@ -283,7 +283,7 @@ namespace MediaBrowser.LocalMetadata.Images { imageFileNames = _seriesImageFileNames; } - else if (item is Video && !(item is Episode)) + else if (item is Video && item is not Episode) { imageFileNames = _videoImageFileNames; } diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs index dd824206f7..6a3896eb6f 100644 --- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs @@ -223,7 +223,7 @@ namespace MediaBrowser.LocalMetadata.Savers writer.WriteElementString("CustomRating", item.CustomRating); } - if (!string.IsNullOrEmpty(item.Name) && !(item is Episode)) + if (!string.IsNullOrEmpty(item.Name) && item is not Episode) { writer.WriteElementString("LocalTitle", item.Name); } @@ -240,7 +240,7 @@ namespace MediaBrowser.LocalMetadata.Savers { writer.WriteElementString("BirthDate", item.PremiereDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); } - else if (!(item is Episode)) + else if (item is not Episode) { writer.WriteElementString("PremiereDate", item.PremiereDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); } @@ -252,7 +252,7 @@ namespace MediaBrowser.LocalMetadata.Savers { writer.WriteElementString("DeathDate", item.EndDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); } - else if (!(item is Episode)) + else if (item is not Episode) { writer.WriteElementString("EndDate", item.EndDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); } @@ -292,7 +292,7 @@ namespace MediaBrowser.LocalMetadata.Savers writer.WriteElementString("Rating", item.CommunityRating.Value.ToString(_usCulture)); } - if (item.ProductionYear.HasValue && !(item is Person)) + if (item.ProductionYear.HasValue && item is not Person) { writer.WriteElementString("ProductionYear", item.ProductionYear.Value.ToString(_usCulture)); } diff --git a/MediaBrowser.Model/Dlna/MediaFormatProfile.cs b/MediaBrowser.Model/Dlna/MediaFormatProfile.cs index 20e05b8a94..06f6660f4b 100644 --- a/MediaBrowser.Model/Dlna/MediaFormatProfile.cs +++ b/MediaBrowser.Model/Dlna/MediaFormatProfile.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591, CA1707 namespace MediaBrowser.Model.Dlna { diff --git a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs index 65fccbdd41..806877ff0b 100644 --- a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs +++ b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs @@ -21,11 +21,7 @@ namespace MediaBrowser.Model.Dlna public static ResolutionOptions Normalize( int? inputBitrate, - int? unused1, - int? unused2, int outputBitrate, - string inputCodec, - string outputCodec, int? maxWidth, int? maxHeight) { diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index f4c69fe8f5..635420a76a 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -694,7 +694,7 @@ namespace MediaBrowser.Model.Dlna if (isEligibleForDirectPlay || isEligibleForDirectStream) { // See if it can be direct played - var directPlayInfo = GetVideoDirectPlayProfile(options, item, videoStream, audioStream, isEligibleForDirectPlay, isEligibleForDirectStream); + var directPlayInfo = GetVideoDirectPlayProfile(options, item, videoStream, audioStream, isEligibleForDirectStream); var directPlay = directPlayInfo.Item1; if (directPlay != null) @@ -810,7 +810,7 @@ namespace MediaBrowser.Model.Dlna // Honor requested max channels playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels; - int audioBitrate = GetAudioBitrate(playlistItem.SubProtocol, options.GetMaxBitrate(false) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem); + int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(false) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem); playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate); isFirstAppliedCodecProfile = true; @@ -907,7 +907,7 @@ namespace MediaBrowser.Model.Dlna return 192000; } - private static int GetAudioBitrate(string subProtocol, long maxTotalBitrate, string[] targetAudioCodecs, MediaStream audioStream, StreamInfo item) + private static int GetAudioBitrate(long maxTotalBitrate, string[] targetAudioCodecs, MediaStream audioStream, StreamInfo item) { string targetAudioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0]; @@ -1005,7 +1005,6 @@ namespace MediaBrowser.Model.Dlna MediaSourceInfo mediaSource, MediaStream videoStream, MediaStream audioStream, - bool isEligibleForDirectPlay, bool isEligibleForDirectStream) { if (options.ForceDirectPlay) @@ -1146,7 +1145,7 @@ namespace MediaBrowser.Model.Dlna { string audioCodec = audioStream.Codec; conditions = new List<ProfileCondition>(); - bool? isSecondaryAudio = audioStream == null ? null : mediaSource.IsSecondaryAudio(audioStream); + bool? isSecondaryAudio = mediaSource.IsSecondaryAudio(audioStream); foreach (var i in profile.CodecProfiles) { @@ -1262,7 +1261,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (playMethod == PlayMethod.Transcode && !IsSubtitleEmbedSupported(subtitleStream, profile, transcodingSubProtocol, outputContainer)) + if (playMethod == PlayMethod.Transcode && !IsSubtitleEmbedSupported(outputContainer)) { continue; } @@ -1291,7 +1290,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (playMethod == PlayMethod.Transcode && !IsSubtitleEmbedSupported(subtitleStream, profile, transcodingSubProtocol, outputContainer)) + if (playMethod == PlayMethod.Transcode && !IsSubtitleEmbedSupported(outputContainer)) { continue; } @@ -1313,7 +1312,7 @@ namespace MediaBrowser.Model.Dlna }; } - private static bool IsSubtitleEmbedSupported(MediaStream subtitleStream, SubtitleProfile subtitleProfile, string transcodingSubProtocol, string transcodingContainer) + private static bool IsSubtitleEmbedSupported(string transcodingContainer) { if (!string.IsNullOrEmpty(transcodingContainer)) { @@ -1728,18 +1727,14 @@ namespace MediaBrowser.Model.Dlna continue; } - if (!string.IsNullOrEmpty(value)) + // change from split by | to comma + // strip spaces to avoid having to encode + var values = value + .Split('|', StringSplitOptions.RemoveEmptyEntries); + + if (condition.Condition == ProfileConditionType.Equals || condition.Condition == ProfileConditionType.EqualsAny) { - // change from split by | to comma - - // strip spaces to avoid having to encode - var values = value - .Split('|', StringSplitOptions.RemoveEmptyEntries); - - if (condition.Condition == ProfileConditionType.Equals || condition.Condition == ProfileConditionType.EqualsAny) - { - item.SetOption(qualifier, "profile", string.Join(',', values)); - } + item.SetOption(qualifier, "profile", string.Join(',', values)); } break; diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 252872847a..432c1c1d60 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -636,7 +636,7 @@ namespace MediaBrowser.Model.Dlna continue; } - var encodedValue = pair.Value.Replace(" ", "%20"); + var encodedValue = pair.Value.Replace(" ", "%20", StringComparison.Ordinal); list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue)); } diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 9653a8ece7..de9fe6ff63 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -261,7 +261,7 @@ namespace MediaBrowser.Model.Entities foreach (var tag in attributes) { // Keep Tags that are not already in Title. - if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1) + if (!Title.Contains(tag, StringComparison.OrdinalIgnoreCase)) { result.Append(" - ").Append(tag); } diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs index be4f1e16b5..0f77d6b5bf 100644 --- a/MediaBrowser.Model/IO/IFileSystem.cs +++ b/MediaBrowser.Model/IO/IFileSystem.cs @@ -51,7 +51,7 @@ namespace MediaBrowser.Model.IO /// <returns>A <see cref="FileSystemMetadata" /> object.</returns> /// <remarks><para>If the specified path points to a directory, the returned <see cref="FileSystemMetadata" /> object's /// <see cref="FileSystemMetadata.IsDirectory" /> property and the <see cref="FileSystemMetadata.Exists" /> property will both be set to false.</para> - /// <para>For automatic handling of files <b>and</b> directories, use <see cref="M:IFileSystem.GetFileSystemInfo(System.String)" />.</para></remarks> + /// <para>For automatic handling of files <b>and</b> directories, use <see cref="GetFileSystemInfo(string)" />.</para></remarks> FileSystemMetadata GetFileInfo(string path); /// <summary> @@ -61,7 +61,7 @@ namespace MediaBrowser.Model.IO /// <returns>A <see cref="FileSystemMetadata" /> object.</returns> /// <remarks><para>If the specified path points to a file, the returned <see cref="FileSystemMetadata" /> object's /// <see cref="FileSystemMetadata.IsDirectory" /> property will be set to true and the <see cref="FileSystemMetadata.Exists" /> property will be set to false.</para> - /// <para>For automatic handling of files <b>and</b> directories, use <see cref="M:IFileSystem.GetFileSystemInfo(System.String)" />.</para></remarks> + /// <para>For automatic handling of files <b>and</b> directories, use <see cref="GetFileSystemInfo(string)" />.</para></remarks> FileSystemMetadata GetDirectoryInfo(string path); /// <summary> diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs index fb1d4f4906..7d259a9d36 100644 --- a/MediaBrowser.Providers/Manager/ImageSaver.cs +++ b/MediaBrowser.Providers/Manager/ImageSaver.cs @@ -91,7 +91,7 @@ namespace MediaBrowser.Providers.Manager throw new ArgumentNullException(nameof(mimeType)); } - var saveLocally = item.SupportsLocalMetadata && item.IsSaveLocalMetadataEnabled() && !item.ExtraType.HasValue && !(item is Audio); + var saveLocally = item.SupportsLocalMetadata && item.IsSaveLocalMetadataEnabled() && !item.ExtraType.HasValue && item is not Audio; if (type != ImageType.Primary && item is Episode) { diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index 607fd127b2..cc4c75d7dd 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -57,7 +57,7 @@ namespace MediaBrowser.Providers.Manager { var hasChanges = false; - if (!(item is Photo)) + if (item is not Photo) { var images = providers.OfType<ILocalImageProvider>() .SelectMany(i => i.GetImages(item, directoryService)) @@ -529,7 +529,7 @@ namespace MediaBrowser.Providers.Manager return true; } - if (item is IItemByName && !(item is MusicArtist)) + if (item is IItemByName && item is not MusicArtist) { var hasDualAccess = item as IHasDualAccess; if (hasDualAccess == null || hasDualAccess.IsAccessedByName) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 3a42eb4c19..ab8d3a2a61 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -584,7 +584,7 @@ namespace MediaBrowser.Providers.Manager protected virtual IEnumerable<IImageProvider> GetNonLocalImageProviders(BaseItem item, IEnumerable<IImageProvider> allImageProviders, ImageRefreshOptions options) { // Get providers to refresh - var providers = allImageProviders.Where(i => !(i is ILocalImageProvider)); + var providers = allImageProviders.Where(i => i is not ILocalImageProvider); var dateLastImageRefresh = item.DateLastRefreshed; @@ -729,7 +729,7 @@ namespace MediaBrowser.Providers.Manager refreshResult.Failures += remoteResult.Failures; } - if (providers.Any(i => !(i is ICustomMetadataProvider))) + if (providers.Any(i => i is not ICustomMetadataProvider)) { if (refreshResult.UpdateType > ItemUpdateType.None) { @@ -748,7 +748,7 @@ namespace MediaBrowser.Providers.Manager // var isUnidentified = failedProviderCount > 0 && successfulProviderCount == 0; - foreach (var provider in customProviders.Where(i => !(i is IPreRefreshProvider))) + foreach (var provider in customProviders.Where(i => i is not IPreRefreshProvider)) { await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwait(false); } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 2dfaa372c5..7e60eced0f 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -323,7 +323,7 @@ namespace MediaBrowser.Providers.Manager .OrderBy(i => { // See if there's a user-defined order - if (!(i is ILocalImageProvider)) + if (i is not ILocalImageProvider) { var fetcherOrder = typeFetcherOrder ?? currentOptions.ImageFetcherOrder; var index = Array.IndexOf(fetcherOrder, i.Name); @@ -390,7 +390,7 @@ namespace MediaBrowser.Providers.Manager if (!includeDisabled) { // If locked only allow local providers - if (item.IsLocked && !(provider is ILocalMetadataProvider) && !(provider is IForcedProvider)) + if (item.IsLocked && provider is not ILocalMetadataProvider && provider is not IForcedProvider) { return false; } @@ -431,7 +431,7 @@ namespace MediaBrowser.Providers.Manager if (!includeDisabled) { // If locked only allow local providers - if (item.IsLocked && !(provider is ILocalImageProvider)) + if (item.IsLocked && provider is not ILocalImageProvider) { if (refreshOptions.ImageRefreshMode != MetadataRefreshMode.FullRefresh) { @@ -466,7 +466,7 @@ namespace MediaBrowser.Providers.Manager /// <returns>System.Int32.</returns> private int GetOrder(IImageProvider provider) { - if (!(provider is IHasOrder hasOrder)) + if (provider is not IHasOrder hasOrder) { return 0; } @@ -745,7 +745,7 @@ namespace MediaBrowser.Providers.Manager { // Manual edit occurred // Even if save local is off, save locally anyway if the metadata file already exists - if (!(saver is IMetadataFileSaver fileSaver) || !File.Exists(fileSaver.GetSavePath(item))) + if (saver is not IMetadataFileSaver fileSaver || !File.Exists(fileSaver.GetSavePath(item))) { return false; } diff --git a/MediaBrowser.Providers/Manager/ProviderUtils.cs b/MediaBrowser.Providers/Manager/ProviderUtils.cs index aceba22158..6d088e6e70 100644 --- a/MediaBrowser.Providers/Manager/ProviderUtils.cs +++ b/MediaBrowser.Providers/Manager/ProviderUtils.cs @@ -135,7 +135,7 @@ namespace MediaBrowser.Providers.Manager { if (replaceData || !target.RunTimeTicks.HasValue) { - if (!(target is Audio) && !(target is Video)) + if (target is not Audio && target is not Video) { target.RunTimeTicks = source.RunTimeTicks; } diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index 3be35e2d9b..38726a6f08 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -555,7 +555,7 @@ namespace MediaBrowser.XbmcMetadata.Savers } // Series xml saver already saves this - if (!(item is Series)) + if (item is not Series) { var tvdb = item.GetProviderId(MetadataProvider.Tvdb); if (!string.IsNullOrEmpty(tvdb)) @@ -582,7 +582,7 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteElementString("countrycode", item.PreferredMetadataCountryCode); } - if (item.PremiereDate.HasValue && !(item is Episode)) + if (item.PremiereDate.HasValue && item is not Episode) { var formatString = options.ReleaseDateFormat; @@ -605,7 +605,7 @@ namespace MediaBrowser.XbmcMetadata.Savers if (item.EndDate.HasValue) { - if (!(item is Episode)) + if (item is not Episode) { var formatString = options.ReleaseDateFormat; diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs index 412e8031b2..21e7e2335b 100644 --- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs @@ -82,7 +82,7 @@ namespace MediaBrowser.XbmcMetadata.Savers } // Check parent for null to avoid running this against things like video backdrops - if (item is Video video && !(item is Episode) && !video.ExtraType.HasValue) + if (item is Video video && item is not Episode && !video.ExtraType.HasValue) { return updateType >= MinimumUpdateType; } diff --git a/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs index b9d73ba822..e975506308 100644 --- a/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs @@ -52,7 +52,7 @@ namespace MediaBrowser.XbmcMetadata.Savers return false; } - if (!(item is Season)) + if (item is not Season) { return false; } From d7459fa2ec41ee80271dde415997c23c2eb97c65 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Sat, 28 Aug 2021 23:02:13 -0600 Subject: [PATCH 64/82] Add missing xmldoc --- .../LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs | 3 +++ .../LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs index b80ac624cc..912e680dd7 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs @@ -4,6 +4,9 @@ using System.Text.Json.Serialization; namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos { + /// <summary> + /// Image data dto. + /// </summary> public class ImageDataDto { /// <summary> diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs index 332edf7c38..34302370d6 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs @@ -5,6 +5,9 @@ using System.Text.Json.Serialization; namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos { + /// <summary> + /// Show image dto. + /// </summary> public class ShowImagesDto { /// <summary> From ecb4b8e0aacef56331e7eadd1f82839b0f6e1e00 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Mon, 30 Aug 2021 07:11:34 -0600 Subject: [PATCH 65/82] Apply suggestions from code review Co-authored-by: Bond-009 <bond.009@outlook.com> --- Emby.Server.Implementations/Data/SqliteItemRepository.cs | 1 - .../Library/Validators/StudiosValidator.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index ab1b55164b..108ea783d4 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -72,7 +72,6 @@ namespace Emby.Server.Implementations.Data _mediaAttachmentInsertPrefix = queryPrefixText.ToString(); } - /// <summary> /// Initializes a new instance of the <see cref="SqliteItemRepository"/> class. /// </summary> diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs index 16bdf720ce..7a22f851b4 100644 --- a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs @@ -87,7 +87,7 @@ namespace Emby.Server.Implementations.Library.Validators foreach (var item in deadEntities) { - _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name); + _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name); _libraryManager.DeleteItem( item, From a0ee16d38d79d385f967a3164e0f2b095ef5deaa Mon Sep 17 00:00:00 2001 From: Mark Titorenkov <mark.titorenkov@outlook.com> Date: Tue, 31 Aug 2021 15:12:09 +0300 Subject: [PATCH 66/82] Update M3U Channel Name Precedence Sets the ExtInf display name to have a higher precedence than the `tvg-name` attribute for channel names. Usually `namInExtInf` is a more descriptive and human readable name if both it and `tvg-name` are available. `tvg-name` is more likely to be an internal identifier such as just the channel number with a prefix in my provider's case. --- Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index 16ff98a7d8..d28c39e213 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -295,11 +295,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts } } - attributes.TryGetValue("tvg-name", out string name); + string name = nameInExtInf; if (string.IsNullOrWhiteSpace(name)) { - name = nameInExtInf; + attributes.TryGetValue("tvg-name", out name); } if (string.IsNullOrWhiteSpace(name)) From cb52ccc6990169d80e0653c7f1bf76b6dc6fd2d6 Mon Sep 17 00:00:00 2001 From: Patrick Barron <18354464+barronpm@users.noreply.github.com> Date: Tue, 31 Aug 2021 10:57:48 -0400 Subject: [PATCH 67/82] Update Jellyfin.Server.Implementations/Devices/DeviceManager.cs Co-authored-by: Claus Vium <cvium@users.noreply.github.com> --- Jellyfin.Server.Implementations/Devices/DeviceManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index 3d1bc30e8a..0655c9813d 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -206,7 +206,7 @@ namespace Jellyfin.Server.Implementations.Devices { if (user == null) { - throw new ArgumentException("user not found"); + throw new ArgumentNullException(nameof(user)); } if (string.IsNullOrEmpty(deviceId)) From 07f64102ddc5b19a0c2e3e9cae308336dad18c5a Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 1 Sep 2021 14:00:06 +0200 Subject: [PATCH 68/82] Fix build --- .../Controllers/MediaStructureControllerTests.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs index 8af43050f6..19d8381ea5 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs @@ -87,10 +87,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers var data = new UpdateMediaPathRequestDto() { Name = " ", - PathInfo = new MediaPathInfo - { - Path = "test" - } + PathInfo = new MediaPathInfo("test") }; using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(data, _jsonOptions)); From 55c8fb7814c3c364b55b879cd5b080442d6f387d Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 1 Sep 2021 14:18:23 +0200 Subject: [PATCH 69/82] Ignore Omnisharp crash logs --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 252210e57c..c2ae76c1e3 100644 --- a/.gitignore +++ b/.gitignore @@ -278,3 +278,6 @@ web/ web-src.* MediaBrowser.WebDashboard/jellyfin-web apiclient/generated + +# Omnisharp crash logs +mono_crash.*.json From cf9c678406b3f412d70637da35ccb0e9f70c3a00 Mon Sep 17 00:00:00 2001 From: Mike <Vagab0nd@users.noreply.github.com> Date: Wed, 1 Sep 2021 18:59:59 +0200 Subject: [PATCH 70/82] Add subtitle format(codec) to stream display title (#5853) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Kurek <michal.kurek@mail.com> --- MediaBrowser.Model/Entities/MediaStream.cs | 5 ++ .../Entities/MediaStreamTests.cs | 80 +++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 9653a8ece7..8955012d7a 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -255,6 +255,11 @@ namespace MediaBrowser.Model.Entities attributes.Add(string.IsNullOrEmpty(LocalizedForced) ? "Forced" : LocalizedForced); } + if (!string.IsNullOrEmpty(Codec)) + { + attributes.Add(Codec.ToUpperInvariant()); + } + if (!string.IsNullOrEmpty(Title)) { var result = new StringBuilder(Title); diff --git a/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs b/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs index e2274e19ee..ce9ecea6a9 100644 --- a/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs +++ b/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using MediaBrowser.Model.Entities; using Xunit; @@ -5,6 +6,85 @@ namespace Jellyfin.Model.Tests.Entities { public class MediaStreamTests { + public static IEnumerable<object[]> Get_DisplayTitle_TestData() + { + return new List<object[]> + { + new object[] + { + new MediaStream + { + Type = MediaStreamType.Subtitle, + Title = "English", + Language = string.Empty, + IsForced = false, + IsDefault = false, + Codec = "ASS" + }, + "English - Und - ASS" + }, + new object[] + { + new MediaStream + { + Type = MediaStreamType.Subtitle, + Title = "English", + Language = string.Empty, + IsForced = false, + IsDefault = false, + Codec = string.Empty + }, + "English - Und" + }, + new object[] + { + new MediaStream + { + Type = MediaStreamType.Subtitle, + Title = "English", + Language = "EN", + IsForced = false, + IsDefault = false, + Codec = string.Empty + }, + "English" + }, + new object[] + { + new MediaStream + { + Type = MediaStreamType.Subtitle, + Title = "English", + Language = "EN", + IsForced = true, + IsDefault = true, + Codec = "SRT" + }, + "English - Default - Forced - SRT" + }, + new object[] + { + new MediaStream + { + Type = MediaStreamType.Subtitle, + Title = null, + Language = null, + IsForced = false, + IsDefault = false, + Codec = null + }, + "Und" + } + }; + } + + [Theory] + [MemberData(nameof(Get_DisplayTitle_TestData))] + public void Get_DisplayTitle_should_return_valid_title(MediaStream mediaStream, string expected) + { + Assert.Equal(expected, mediaStream.DisplayTitle); + } + [Theory] [InlineData(null, null, false, null)] [InlineData(null, 0, false, null)] From 2cf08dcd34db4d749a93de29e3ba13c633938e0c Mon Sep 17 00:00:00 2001 From: qsniyg <qsniyg@protonmail.com> Date: Wed, 1 Sep 2021 15:52:59 -0700 Subject: [PATCH 71/82] Allow zero activity log retention days --- .../ScheduledTasks/Tasks/CleanActivityLogTask.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs index 19600b1e64..79886cb52c 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs @@ -60,7 +60,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks public Task Execute(CancellationToken cancellationToken, IProgress<double> progress) { var retentionDays = _serverConfigurationManager.Configuration.ActivityLogRetentionDays; - if (!retentionDays.HasValue || retentionDays <= 0) + if (!retentionDays.HasValue || retentionDays < 0) { throw new Exception($"Activity Log Retention days must be at least 0. Currently: {retentionDays}"); } From 4bed19e22fb2f548d001303a1f9a44475641aec6 Mon Sep 17 00:00:00 2001 From: Gokdag Goktepe <gokdag.goktepe@gmail.com> Date: Wed, 1 Sep 2021 07:59:23 +0000 Subject: [PATCH 72/82] Translated using Weblate (Turkish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/tr/ --- Emby.Server.Implementations/Localization/Core/tr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index 771c91d59f..e661299c41 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -25,7 +25,7 @@ "HeaderLiveTV": "Canlı TV", "HeaderNextUp": "Gelecek Hafta", "HeaderRecordingGroups": "Kayıt Grupları", - "HomeVideos": "Ev videoları", + "HomeVideos": "Ana sayfa videoları", "Inherit": "Devral", "ItemAddedWithName": "{0} kütüphaneye eklendi", "ItemRemovedWithName": "{0} kütüphaneden silindi", From 286dabdc4bcff65430f0abe78fbeaaed28635e18 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Thu, 2 Sep 2021 21:28:00 +0200 Subject: [PATCH 73/82] Add SqliteItemRepository.ItemImageInfoFromValueString as a fuzzing target and add test cases --- .../Data/SqliteItemRepository.cs | 12 +++++++- .../Properties/AssemblyInfo.cs | 1 + .../Emby.Server.Implementations.Fuzz.csproj | 7 +++++ .../Program.cs | 30 +++++++++++++++++++ .../test1.txt | 1 + .../Data/SqliteItemRepositoryTests.cs | 3 ++ 6 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 fuzz/Emby.Server.Implementations.Fuzz/Testcases/SqliteItemRepository.ItemImageInfoFromValueString/test1.txt diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 2cb10765ff..93d527a4d1 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -1135,15 +1135,25 @@ namespace Emby.Server.Implementations.Data Path = RestorePath(path.ToString()) }; - if (long.TryParse(dateModified, NumberStyles.Any, CultureInfo.InvariantCulture, out var ticks)) + if (long.TryParse(dateModified, NumberStyles.Any, CultureInfo.InvariantCulture, out var ticks) + && ticks >= DateTime.MinValue.Ticks + && ticks <= DateTime.MaxValue.Ticks) { image.DateModified = new DateTime(ticks, DateTimeKind.Utc); } + else + { + return null; + } if (Enum.TryParse(imageType.ToString(), true, out ImageType type)) { image.Type = type; } + else + { + return null; + } // Optional parameters: width*height*blurhash if (nextSegment + 1 < value.Length - 1) diff --git a/Emby.Server.Implementations/Properties/AssemblyInfo.cs b/Emby.Server.Implementations/Properties/AssemblyInfo.cs index cb7972173e..41c396ac1d 100644 --- a/Emby.Server.Implementations/Properties/AssemblyInfo.cs +++ b/Emby.Server.Implementations/Properties/AssemblyInfo.cs @@ -16,6 +16,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] [assembly: InternalsVisibleTo("Jellyfin.Server.Implementations.Tests")] +[assembly: InternalsVisibleTo("Emby.Server.Implementations.Fuzz")] // 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 diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj index 791cb140db..6abdb77342 100644 --- a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj +++ b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj @@ -12,6 +12,13 @@ </ItemGroup> <ItemGroup> + <ProjectReference Include="../../MediaBrowser.Controller/MediaBrowser.Controller.csproj" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="AutoFixture" Version="4.17.0" /> + <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> + <PackageReference Include="Moq" Version="4.16.1" /> <PackageReference Include="SharpFuzz" Version="1.6.2" /> </ItemGroup> diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Program.cs b/fuzz/Emby.Server.Implementations.Fuzz/Program.cs index a4a6f5f54d..03b2964948 100644 --- a/fuzz/Emby.Server.Implementations.Fuzz/Program.cs +++ b/fuzz/Emby.Server.Implementations.Fuzz/Program.cs @@ -1,5 +1,12 @@ using System; +using AutoFixture; +using AutoFixture.AutoMoq; +using Emby.Server.Implementations.Data; using Emby.Server.Implementations.Library; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; +using Moq; using SharpFuzz; namespace Emby.Server.Implementations.Fuzz @@ -11,6 +18,7 @@ namespace Emby.Server.Implementations.Fuzz switch (args[0]) { case "PathExtensions.TryReplaceSubPath": Run(PathExtensions_TryReplaceSubPath); return; + case "SqliteItemRepository.ItemImageInfoFromValueString": Run(SqliteItemRepository_ItemImageInfoFromValueString); return; default: throw new ArgumentException($"Unknown fuzzing function: {args[0]}"); } } @@ -28,5 +36,27 @@ namespace Emby.Server.Implementations.Fuzz _ = PathExtensions.TryReplaceSubPath(parts[0], parts[1], parts[2], out _); } + + private static void SqliteItemRepository_ItemImageInfoFromValueString(string data) + { + var sqliteItemRepository = MockSqliteItemRepository(); + sqliteItemRepository.ItemImageInfoFromValueString(data); + } + + private static SqliteItemRepository MockSqliteItemRepository() + { + const string VirtualMetaDataPath = "%MetadataPath%"; + const string MetaDataPath = "/meta/data/path"; + + var appHost = new Mock<IServerApplicationHost>(); + appHost.Setup(x => x.ExpandVirtualPath(It.IsAny<string>())) + .Returns((string x) => x.Replace(VirtualMetaDataPath, MetaDataPath, StringComparison.Ordinal)); + appHost.Setup(x => x.ReverseVirtualPath(It.IsAny<string>())) + .Returns((string x) => x.Replace(MetaDataPath, VirtualMetaDataPath, StringComparison.Ordinal)); + + IFixture fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true }); + fixture.Inject(appHost); + return fixture.Create<SqliteItemRepository>(); + } } } diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Testcases/SqliteItemRepository.ItemImageInfoFromValueString/test1.txt b/fuzz/Emby.Server.Implementations.Fuzz/Testcases/SqliteItemRepository.ItemImageInfoFromValueString/test1.txt new file mode 100644 index 0000000000..1b0115882f --- /dev/null +++ b/fuzz/Emby.Server.Implementations.Fuzz/Testcases/SqliteItemRepository.ItemImageInfoFromValueString/test1.txt @@ -0,0 +1 @@ +/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN diff --git a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs index f312933fbf..a6e1dfe8f6 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs @@ -109,6 +109,9 @@ namespace Jellyfin.Server.Implementations.Tests.Data [InlineData("")] [InlineData("*")] [InlineData("https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0")] + [InlineData("/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*6374520964785129080*WjQbtJtSO8nhNZ%L_Io#R/oaS<o}-;adXAoIn7j[%hW9s:WGw[nN")] // Invalid modified date + [InlineData("/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*-637452096478512963*WjQbtJtSO8nhNZ%L_Io#R/oaS<o}-;adXAoIn7j[%hW9s:WGw[nN")] // Negative modified date + [InlineData("/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Invalid*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN")] // Invalid type public void ItemImageInfoFromValueString_Invalid_Null(string value) { Assert.Null(_sqliteItemRepository.ItemImageInfoFromValueString(value)); From 098bd5cfa9dd16bde95c597f1245a69f0d2a74ac Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Thu, 2 Sep 2021 20:18:46 -0400 Subject: [PATCH 74/82] Remove IAuditableEntity --- Jellyfin.Data/Entities/Security/Device.cs | 11 +++++++---- Jellyfin.Data/Interfaces/IAuditableEntity.cs | 20 -------------------- 2 files changed, 7 insertions(+), 24 deletions(-) delete mode 100644 Jellyfin.Data/Interfaces/IAuditableEntity.cs diff --git a/Jellyfin.Data/Entities/Security/Device.cs b/Jellyfin.Data/Entities/Security/Device.cs index 9490323b19..67d7f78eda 100644 --- a/Jellyfin.Data/Entities/Security/Device.cs +++ b/Jellyfin.Data/Entities/Security/Device.cs @@ -2,14 +2,13 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Globalization; -using Jellyfin.Data.Interfaces; namespace Jellyfin.Data.Entities.Security { /// <summary> /// An entity representing a device. /// </summary> - public class Device : IAuditableEntity + public class Device { /// <summary> /// Initializes a new instance of the <see cref="Device"/> class. @@ -85,10 +84,14 @@ namespace Jellyfin.Data.Entities.Security /// </summary> public bool IsActive { get; set; } - /// <inheritdoc /> + /// <summary> + /// Gets or sets the date created. + /// </summary> public DateTime DateCreated { get; set; } - /// <inheritdoc /> + /// <summary> + /// Gets or sets the date modified. + /// </summary> public DateTime DateModified { get; set; } /// <summary> diff --git a/Jellyfin.Data/Interfaces/IAuditableEntity.cs b/Jellyfin.Data/Interfaces/IAuditableEntity.cs deleted file mode 100644 index 4420446ae6..0000000000 --- a/Jellyfin.Data/Interfaces/IAuditableEntity.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace Jellyfin.Data.Interfaces -{ - /// <summary> - /// An interface representing an entity that has creation/modification dates. - /// </summary> - public interface IAuditableEntity - { - /// <summary> - /// Gets the date this entity was created. - /// </summary> - public DateTime DateCreated { get; } - - /// <summary> - /// Gets or sets the date this entity was modified. - /// </summary> - public DateTime DateModified { get; set; } - } -} From 88157fcc77dc6cd47b4defda95089677403241ba Mon Sep 17 00:00:00 2001 From: Patrick Barron <barronpm@gmail.com> Date: Thu, 2 Sep 2021 20:22:08 -0400 Subject: [PATCH 75/82] Re-add documentation --- MediaBrowser.Controller/Devices/IDeviceManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index 7e696c3b38..8362db1a71 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -68,6 +68,9 @@ namespace MediaBrowser.Controller.Devices /// <summary> /// Determines whether this instance [can access device] the specified user identifier. /// </summary> + /// <param name="user">The user to test.</param> + /// <param name="deviceId">The device id to test.</param> + /// <returns>Whether the user can access the device.</returns> bool CanAccessDevice(User user, string deviceId); Task UpdateDeviceOptions(string deviceId, string deviceName); From 8e498b01e73b84f4343f8a00df11869ab8ab7089 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Fri, 3 Sep 2021 06:57:04 -0600 Subject: [PATCH 76/82] Fix issues from merge --- Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index bcfc4011c5..8125ed57df 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -15,8 +15,8 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos; -using MediaBrowser.Common; using Jellyfin.Extensions.Json; +using MediaBrowser.Common; using MediaBrowser.Common.Net; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Cryptography; From 611c20dba88ba83e41cb421baa12aa238af76216 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Fri, 3 Sep 2021 06:57:33 -0600 Subject: [PATCH 77/82] Fix indentation --- .../Library/Validators/StudiosValidator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs index 7a22f851b4..8577d722e0 100644 --- a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs @@ -92,9 +92,9 @@ namespace Emby.Server.Implementations.Library.Validators _libraryManager.DeleteItem( item, new DeleteOptions - { - DeleteFileLocation = false - }, + { + DeleteFileLocation = false + }, false); } From 3f2c7065756682c96c90189ab5157b96fdb92bd0 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Fri, 3 Sep 2021 07:19:50 -0600 Subject: [PATCH 78/82] Apply suggestions from code review Co-authored-by: Claus Vium <cvium@users.noreply.github.com> --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 5444e89050..1be3a1ca43 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -3306,7 +3306,7 @@ namespace MediaBrowser.Controller.MediaEncoding inputModifier += " " + videoDecoder; if (!IsCopyCodec(state.OutputVideoCodec) - && videoDecoder.IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1) + && videoDecoder.Contains("cuvid", StringComparison.OrdinalIgnoreCase)) { var videoStream = state.VideoStream; var inputWidth = videoStream?.Width; @@ -3315,7 +3315,7 @@ namespace MediaBrowser.Controller.MediaEncoding var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight); - if (videoDecoder.IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1 + if (videoDecoder.Contains("cuvid", StringComparison.OrdinalIgnoreCase) && width.HasValue && height.HasValue) { From bfb37a9ed9989f63ee8b00f41f4efef7a92fc157 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Fri, 3 Sep 2021 07:21:21 -0600 Subject: [PATCH 79/82] Fix typos --- .../LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs | 2 +- .../LiveTv/Listings/SchedulesDirectDtos/MapDto.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs index b8f27a8ac7..52e920aa61 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs @@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos /// <summary> /// Gets or sets the transport. /// </summary> - [JsonPropertyName("transport.")] + [JsonPropertyName("transport")] public string Transport { get; set; } /// <summary> diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs index 8d45e8fff7..5140277b2c 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs @@ -34,15 +34,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos public int UhfVhf { get; set; } /// <summary> - /// Gets or sets the astc major. + /// Gets or sets the atsc major. /// </summary> - [JsonPropertyName("astcMajor")] + [JsonPropertyName("atscMajor")] public int AtscMajor { get; set; } /// <summary> - /// Gets or sets the astc minor. + /// Gets or sets the atsc minor. /// </summary> - [JsonPropertyName("astcMinor")] + [JsonPropertyName("atscMinor")] public int AtscMinor { get; set; } } } From 2f360151c266ff0885589338fdcdebd2a0c57fae Mon Sep 17 00:00:00 2001 From: siankatabg <siankata91@gmail.com> Date: Thu, 2 Sep 2021 13:17:12 +0000 Subject: [PATCH 80/82] Translated using Weblate (Bulgarian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/bg/ --- Emby.Server.Implementations/Localization/Core/bg-BG.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json index f55287d15b..e1c9233087 100644 --- a/Emby.Server.Implementations/Localization/Core/bg-BG.json +++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json @@ -25,7 +25,7 @@ "HeaderLiveTV": "Телевизия на живо", "HeaderNextUp": "Следва", "HeaderRecordingGroups": "Запис групи", - "HomeVideos": "Домашни клипове", + "HomeVideos": "Домашни Клипове", "Inherit": "Наследяване", "ItemAddedWithName": "{0} е добавено към библиотеката", "ItemRemovedWithName": "{0} е премахнато от библиотеката", @@ -39,7 +39,7 @@ "MixedContent": "Смесено съдържание", "Movies": "Филми", "Music": "Музика", - "MusicVideos": "Музикални видеа", + "MusicVideos": "Музикални Видеа", "NameInstallFailed": "{0} не можа да се инсталира", "NameSeasonNumber": "Сезон {0}", "NameSeasonUnknown": "Неразпознат сезон", From b458f85c47e5f63e53062b8c96c564c674bb62dc Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Fri, 3 Sep 2021 16:39:03 +0200 Subject: [PATCH 81/82] Fix InvalidOperationException when serializing MediaPathInfo --- MediaBrowser.Model/Configuration/MediaPathInfo.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/MediaBrowser.Model/Configuration/MediaPathInfo.cs b/MediaBrowser.Model/Configuration/MediaPathInfo.cs index d096defcb8..a7bc435901 100644 --- a/MediaBrowser.Model/Configuration/MediaPathInfo.cs +++ b/MediaBrowser.Model/Configuration/MediaPathInfo.cs @@ -9,6 +9,12 @@ namespace MediaBrowser.Model.Configuration Path = path; } + // Needed for xml serialization + public MediaPathInfo() + { + Path = string.Empty; + } + public string Path { get; set; } public string? NetworkPath { get; set; } From 637e86478f5cca7c8ac5e17cf541dc4c6adac14e Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Fri, 3 Sep 2021 18:46:34 +0200 Subject: [PATCH 82/82] Fix some warnings --- .../ApplicationHost.cs | 326 ++++++++---------- .../Collections/CollectionManager.cs | 4 +- .../Data/SqliteItemRepository.cs | 17 +- .../Data/SqliteUserDataRepository.cs | 47 ++- Emby.Server.Implementations/Dto/DtoService.cs | 14 +- .../IO/ManagedFileSystem.cs | 6 +- .../Images/BaseDynamicImageProvider.cs | 2 +- .../Library/LibraryManager.cs | 22 +- .../Library/MusicManager.cs | 5 +- .../Library/Resolvers/BaseVideoResolver.cs | 4 +- .../Library/UserDataManager.cs | 4 +- .../LiveTv/EmbyTV/EmbyTV.cs | 26 +- .../LiveTv/LiveTvManager.cs | 55 ++- .../LiveTv/TunerHosts/BaseTunerHost.cs | 2 +- .../TunerHosts/HdHomerun/HdHomerunHost.cs | 64 ++-- .../LiveTv/TunerHosts/M3UTunerHost.cs | 22 +- .../Net/SocketFactory.cs | 20 +- Emby.Server.Implementations/Net/UdpSocket.cs | 4 +- .../Tasks/DeleteCacheFileTask.cs | 4 + .../Session/SessionManager.cs | 18 +- .../TV/TVSeriesManager.cs | 22 +- .../Entities/CollectionFolder.cs | 3 +- .../IServerApplicationHost.cs | 7 - .../Library/ILibraryManager.cs | 6 +- .../Library/IUserDataManager.cs | 6 +- .../LiveTv/ILiveTvManager.cs | 2 +- .../LiveTv/ILiveTvService.cs | 4 +- .../Persistence/IItemRepository.cs | 8 +- .../Session/ISessionManager.cs | 2 +- MediaBrowser.Model/Dlna/StreamInfo.cs | 85 ++--- jellyfin.ruleset | 6 + 31 files changed, 366 insertions(+), 451 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 39e59a0739..3a504d2f43 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -114,6 +114,11 @@ namespace Emby.Server.Implementations /// </summary> private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" }; + /// <summary> + /// The disposable parts. + /// </summary> + private readonly List<IDisposable> _disposableParts = new List<IDisposable>(); + private readonly IFileSystem _fileSystemManager; private readonly IConfiguration _startupConfig; private readonly IXmlSerializer _xmlSerializer; @@ -125,104 +130,15 @@ namespace Emby.Server.Implementations private ISessionManager _sessionManager; private string[] _urlPrefixes; - /// <summary> - /// Gets a value indicating whether this instance can self restart. - /// </summary> - public bool CanSelfRestart => _startupOptions.RestartPath != null; - - public bool CoreStartupHasCompleted { get; private set; } - - public virtual bool CanLaunchWebBrowser - { - get - { - if (!Environment.UserInteractive) - { - return false; - } - - if (_startupOptions.IsService) - { - return false; - } - - return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS(); - } - } - - /// <summary> - /// Gets the <see cref="INetworkManager"/> singleton instance. - /// </summary> - public INetworkManager NetManager { get; internal set; } - - /// <summary> - /// Occurs when [has pending restart changed]. - /// </summary> - public event EventHandler HasPendingRestartChanged; - - /// <summary> - /// Gets a value indicating whether this instance has changes that require the entire application to restart. - /// </summary> - /// <value><c>true</c> if this instance has pending application restart; otherwise, <c>false</c>.</value> - public bool HasPendingRestart { get; private set; } - - /// <inheritdoc /> - public bool IsShuttingDown { get; private set; } - - /// <summary> - /// Gets the logger. - /// </summary> - protected ILogger<ApplicationHost> Logger { get; } - - protected IServiceCollection ServiceCollection { get; } - - /// <summary> - /// Gets the logger factory. - /// </summary> - protected ILoggerFactory LoggerFactory { get; } - - /// <summary> - /// Gets or sets the application paths. - /// </summary> - /// <value>The application paths.</value> - protected IServerApplicationPaths ApplicationPaths { get; set; } - /// <summary> /// Gets or sets all concrete types. /// </summary> /// <value>All concrete types.</value> private Type[] _allConcreteTypes; - /// <summary> - /// The disposable parts. - /// </summary> - private readonly List<IDisposable> _disposableParts = new List<IDisposable>(); + private DeviceId _deviceId; - /// <summary> - /// Gets or sets the configuration manager. - /// </summary> - /// <value>The configuration manager.</value> - public ServerConfigurationManager ConfigurationManager { get; set; } - - /// <summary> - /// Gets or sets the service provider. - /// </summary> - public IServiceProvider ServiceProvider { get; set; } - - /// <summary> - /// Gets the http port for the webhost. - /// </summary> - public int HttpPort { get; private set; } - - /// <summary> - /// Gets the https port for the webhost. - /// </summary> - public int HttpsPort { get; private set; } - - /// <summary> - /// Gets the value of the PublishedServerUrl setting. - /// </summary> - public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey]; + private bool _disposed = false; /// <summary> /// Initializes a new instance of the <see cref="ApplicationHost"/> class. @@ -265,6 +181,143 @@ namespace Emby.Server.Implementations ApplicationVersion); } + /// <summary> + /// Occurs when [has pending restart changed]. + /// </summary> + public event EventHandler HasPendingRestartChanged; + + /// <summary> + /// Gets a value indicating whether this instance can self restart. + /// </summary> + public bool CanSelfRestart => _startupOptions.RestartPath != null; + + public bool CoreStartupHasCompleted { get; private set; } + + public virtual bool CanLaunchWebBrowser + { + get + { + if (!Environment.UserInteractive) + { + return false; + } + + if (_startupOptions.IsService) + { + return false; + } + + return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS(); + } + } + + /// <summary> + /// Gets the <see cref="INetworkManager"/> singleton instance. + /// </summary> + public INetworkManager NetManager { get; internal set; } + + /// <summary> + /// Gets a value indicating whether this instance has changes that require the entire application to restart. + /// </summary> + /// <value><c>true</c> if this instance has pending application restart; otherwise, <c>false</c>.</value> + public bool HasPendingRestart { get; private set; } + + /// <inheritdoc /> + public bool IsShuttingDown { get; private set; } + + /// <summary> + /// Gets the logger. + /// </summary> + protected ILogger<ApplicationHost> Logger { get; } + + protected IServiceCollection ServiceCollection { get; } + + /// <summary> + /// Gets the logger factory. + /// </summary> + protected ILoggerFactory LoggerFactory { get; } + + /// <summary> + /// Gets or sets the application paths. + /// </summary> + /// <value>The application paths.</value> + protected IServerApplicationPaths ApplicationPaths { get; set; } + + /// <summary> + /// Gets or sets the configuration manager. + /// </summary> + /// <value>The configuration manager.</value> + public ServerConfigurationManager ConfigurationManager { get; set; } + + /// <summary> + /// Gets or sets the service provider. + /// </summary> + public IServiceProvider ServiceProvider { get; set; } + + /// <summary> + /// Gets the http port for the webhost. + /// </summary> + public int HttpPort { get; private set; } + + /// <summary> + /// Gets the https port for the webhost. + /// </summary> + public int HttpsPort { get; private set; } + + /// <summary> + /// Gets the value of the PublishedServerUrl setting. + /// </summary> + public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey]; + + /// <inheritdoc /> + public Version ApplicationVersion { get; } + + /// <inheritdoc /> + public string ApplicationVersionString { get; } + + /// <summary> + /// Gets the current application user agent. + /// </summary> + /// <value>The application user agent.</value> + public string ApplicationUserAgent { get; } + + /// <summary> + /// Gets the email address for use within a comment section of a user agent field. + /// Presently used to provide contact information to MusicBrainz service. + /// </summary> + public string ApplicationUserAgentAddress => "team@jellyfin.org"; + + /// <summary> + /// Gets the current application name. + /// </summary> + /// <value>The application name.</value> + public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName; + + public string SystemId + { + get + { + _deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory); + + return _deviceId.Value; + } + } + + /// <inheritdoc/> + public string Name => ApplicationProductName; + + private CertificateInfo CertificateInfo { get; set; } + + public X509Certificate2 Certificate { get; private set; } + + /// <inheritdoc/> + public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps; + + public string FriendlyName => + string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName) + ? Environment.MachineName + : ConfigurationManager.Configuration.ServerName; + /// <summary> /// Temporary function to migration network settings out of system.xml and into network.xml. /// TODO: remove at the point when a fixed migration path has been decided upon. @@ -297,45 +350,6 @@ namespace Emby.Server.Implementations .Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase); } - /// <inheritdoc /> - public Version ApplicationVersion { get; } - - /// <inheritdoc /> - public string ApplicationVersionString { get; } - - /// <summary> - /// Gets the current application user agent. - /// </summary> - /// <value>The application user agent.</value> - public string ApplicationUserAgent { get; } - - /// <summary> - /// Gets the email address for use within a comment section of a user agent field. - /// Presently used to provide contact information to MusicBrainz service. - /// </summary> - public string ApplicationUserAgentAddress => "team@jellyfin.org"; - - /// <summary> - /// Gets the current application name. - /// </summary> - /// <value>The application name.</value> - public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName; - - private DeviceId _deviceId; - - public string SystemId - { - get - { - _deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory); - - return _deviceId.Value; - } - } - - /// <inheritdoc/> - public string Name => ApplicationProductName; - /// <summary> /// Creates an instance of type and resolves all constructor dependencies. /// </summary> @@ -857,10 +871,6 @@ namespace Emby.Server.Implementations } } - private CertificateInfo CertificateInfo { get; set; } - - public X509Certificate2 Certificate { get; private set; } - private IEnumerable<string> GetUrlPrefixes() { var hosts = new[] { "+" }; @@ -1114,9 +1124,6 @@ namespace Emby.Server.Implementations }; } - /// <inheritdoc/> - public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps; - /// <inheritdoc/> public string GetSmartApiUrl(IPAddress remoteAddr, int? port = null) { @@ -1203,14 +1210,7 @@ namespace Emby.Server.Implementations }.ToString().TrimEnd('/'); } - public string FriendlyName => - string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName) - ? Environment.MachineName - : ConfigurationManager.Configuration.ServerName; - - /// <summary> - /// Shuts down. - /// </summary> + /// <inheritdoc /> public async Task Shutdown() { if (IsShuttingDown) @@ -1248,41 +1248,7 @@ namespace Emby.Server.Implementations } } - public virtual void LaunchUrl(string url) - { - if (!CanLaunchWebBrowser) - { - throw new NotSupportedException(); - } - - var process = new Process - { - StartInfo = new ProcessStartInfo - { - FileName = url, - UseShellExecute = true, - ErrorDialog = false - }, - EnableRaisingEvents = true - }; - process.Exited += (sender, args) => ((Process)sender).Dispose(); - - try - { - process.Start(); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error launching url: {url}", url); - throw; - } - } - - private bool _disposed = false; - - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> + /// <inheritdoc /> public void Dispose() { Dispose(true); diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index b00a519229..79ef70fffd 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -196,8 +196,8 @@ namespace Emby.Server.Implementations.Collections } /// <inheritdoc /> - public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids) - => AddToCollectionAsync(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem))); + public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds) + => AddToCollectionAsync(collectionId, itemIds, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem))); private async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefreshOptions refreshOptions) { diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 0e6b7fb82d..30f88c7964 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -1902,12 +1902,7 @@ namespace Emby.Server.Implementations.Data return result; } - /// <summary> - /// Gets chapters for an item. - /// </summary> - /// <param name="item">The item.</param> - /// <returns>IEnumerable{ChapterInfo}.</returns> - /// <exception cref="ArgumentNullException">id</exception> + /// <inheritdoc /> public List<ChapterInfo> GetChapters(BaseItem item) { CheckDisposed(); @@ -1930,13 +1925,7 @@ namespace Emby.Server.Implementations.Data } } - /// <summary> - /// Gets a single chapter for an item. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="index">The index.</param> - /// <returns>ChapterInfo.</returns> - /// <exception cref="ArgumentNullException">id</exception> + /// <inheritdoc /> public ChapterInfo GetChapter(BaseItem item, int index) { CheckDisposed(); @@ -2048,7 +2037,7 @@ namespace Emby.Server.Implementations.Data for (var i = startIndex; i < endIndex; i++) { - insertText.AppendFormat("(@ItemId, @ChapterIndex{0}, @StartPositionTicks{0}, @Name{0}, @ImagePath{0}, @ImageDateModified{0}),", i.ToString(CultureInfo.InvariantCulture)); + insertText.AppendFormat(CultureInfo.InvariantCulture, "(@ItemId, @ChapterIndex{0}, @StartPositionTicks{0}, @Name{0}, @ImagePath{0}, @ImageDateModified{0}),", i.ToString(CultureInfo.InvariantCulture)); } insertText.Length -= 1; // Remove last , diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 613d07d775..829f1de2f6 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -129,19 +129,17 @@ namespace Emby.Server.Implementations.Data return list; } - /// <summary> - /// Saves the user data. - /// </summary> - public void SaveUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken) + /// <inheritdoc /> + public void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken) { if (userData == null) { throw new ArgumentNullException(nameof(userData)); } - if (internalUserId <= 0) + if (userId <= 0) { - throw new ArgumentNullException(nameof(internalUserId)); + throw new ArgumentNullException(nameof(userId)); } if (string.IsNullOrEmpty(key)) @@ -149,22 +147,23 @@ namespace Emby.Server.Implementations.Data throw new ArgumentNullException(nameof(key)); } - PersistUserData(internalUserId, key, userData, cancellationToken); + PersistUserData(userId, key, userData, cancellationToken); } - public void SaveAllUserData(long internalUserId, UserItemData[] userData, CancellationToken cancellationToken) + /// <inheritdoc /> + public void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken) { if (userData == null) { throw new ArgumentNullException(nameof(userData)); } - if (internalUserId <= 0) + if (userId <= 0) { - throw new ArgumentNullException(nameof(internalUserId)); + throw new ArgumentNullException(nameof(userId)); } - PersistAllUserData(internalUserId, userData, cancellationToken); + PersistAllUserData(userId, userData, cancellationToken); } /// <summary> @@ -263,19 +262,19 @@ namespace Emby.Server.Implementations.Data /// <summary> /// Gets the user data. /// </summary> - /// <param name="internalUserId">The user id.</param> + /// <param name="userId">The user id.</param> /// <param name="key">The key.</param> /// <returns>Task{UserItemData}.</returns> /// <exception cref="ArgumentNullException"> /// userId /// or - /// key + /// key. /// </exception> - public UserItemData GetUserData(long internalUserId, string key) + public UserItemData GetUserData(long userId, string key) { - if (internalUserId <= 0) + if (userId <= 0) { - throw new ArgumentNullException(nameof(internalUserId)); + throw new ArgumentNullException(nameof(userId)); } if (string.IsNullOrEmpty(key)) @@ -287,7 +286,7 @@ namespace Emby.Server.Implementations.Data { 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("@UserId", userId); statement.TryBind("@Key", key); foreach (var row in statement.ExecuteQuery()) @@ -300,7 +299,7 @@ namespace Emby.Server.Implementations.Data } } - public UserItemData GetUserData(long internalUserId, List<string> keys) + public UserItemData GetUserData(long userId, List<string> keys) { if (keys == null) { @@ -312,19 +311,19 @@ namespace Emby.Server.Implementations.Data return null; } - return GetUserData(internalUserId, keys[0]); + return GetUserData(userId, keys[0]); } /// <summary> /// Return all user-data associated with the given user. /// </summary> - /// <param name="internalUserId">The internal user id.</param> + /// <param name="userId">The internal user id.</param> /// <returns>The list of user item data.</returns> - public List<UserItemData> GetAllUserData(long internalUserId) + public List<UserItemData> GetAllUserData(long userId) { - if (internalUserId <= 0) + if (userId <= 0) { - throw new ArgumentNullException(nameof(internalUserId)); + throw new ArgumentNullException(nameof(userId)); } var list = new List<UserItemData>(); @@ -333,7 +332,7 @@ namespace Emby.Server.Implementations.Data { 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); + statement.TryBind("@UserId", userId); foreach (var row in statement.ExecuteQuery()) { diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 7c2d02037c..74400b5128 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -51,8 +51,6 @@ namespace Emby.Server.Implementations.Dto private readonly IMediaSourceManager _mediaSourceManager; private readonly Lazy<ILiveTvManager> _livetvManagerFactory; - private ILiveTvManager LivetvManager => _livetvManagerFactory.Value; - public DtoService( ILogger<DtoService> logger, ILibraryManager libraryManager, @@ -75,6 +73,8 @@ namespace Emby.Server.Implementations.Dto _livetvManagerFactory = livetvManagerFactory; } + private ILiveTvManager LivetvManager => _livetvManagerFactory.Value; + /// <inheritdoc /> public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null) { @@ -507,7 +507,6 @@ namespace Emby.Server.Implementations.Dto /// </summary> /// <param name="dto">The dto.</param> /// <param name="item">The item.</param> - /// <returns>Task.</returns> private void AttachPeople(BaseItemDto dto, BaseItem item) { // Ordering by person type to ensure actors and artists are at the front. @@ -616,7 +615,6 @@ namespace Emby.Server.Implementations.Dto /// </summary> /// <param name="dto">The dto.</param> /// <param name="item">The item.</param> - /// <returns>Task.</returns> private void AttachStudios(BaseItemDto dto, BaseItem item) { dto.Studios = item.Studios @@ -1313,9 +1311,12 @@ namespace Emby.Server.Implementations.Dto var imageTags = dto.ImageTags; - while (((!(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0) || (!(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0) || (!(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0) || parent is Series) && - (parent ??= (isFirst ? GetImageDisplayParent(item, item) ?? owner : parent)) != null) + while ((!(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0) + || (!(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0) + || (!(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0) + || parent is Series) { + parent ??= isFirst ? GetImageDisplayParent(item, item) ?? owner : parent; if (parent == null) { break; @@ -1395,7 +1396,6 @@ namespace Emby.Server.Implementations.Dto /// </summary> /// <param name="dto">The dto.</param> /// <param name="item">The item.</param> - /// <returns>Task.</returns> public void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item) { dto.PrimaryImageAspectRatio = GetPrimaryImageAspectRatio(item); diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 7c3c7da230..2c722ff25a 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -423,7 +423,7 @@ namespace Emby.Server.Implementations.IO } } - public virtual void SetAttributes(string path, bool isHidden, bool isReadOnly) + public virtual void SetAttributes(string path, bool isHidden, bool readOnly) { if (!OperatingSystem.IsWindows()) { @@ -437,14 +437,14 @@ namespace Emby.Server.Implementations.IO return; } - if (info.IsReadOnly == isReadOnly && info.IsHidden == isHidden) + if (info.IsReadOnly == readOnly && info.IsHidden == isHidden) { return; } var attributes = File.GetAttributes(path); - if (isReadOnly) + if (readOnly) { attributes = attributes | FileAttributes.ReadOnly; } diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs index 833fb0b7a1..4a026fd218 100644 --- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs @@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.Images public int Order => 0; - protected virtual bool Supports(BaseItem _) => true; + protected virtual bool Supports(BaseItem item) => true; public async Task<ItemUpdateType> FetchAsync(T item, MetadataRefreshOptions options, CancellationToken cancellationToken) { diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index a0a6bb2929..8054beae3f 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1761,22 +1761,20 @@ namespace Emby.Server.Implementations.Library return orderedItems ?? items; } - public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ValueTuple<string, SortOrder>> orderByList) + public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ValueTuple<string, SortOrder>> orderBy) { var isFirst = true; IOrderedEnumerable<BaseItem> orderedItems = null; - foreach (var orderBy in orderByList) + foreach (var (name, sortOrder) in orderBy) { - var comparer = GetComparer(orderBy.Item1, user); + var comparer = GetComparer(name, user); if (comparer == null) { continue; } - var sortOrder = orderBy.Item2; - if (isFirst) { orderedItems = sortOrder == SortOrder.Descending ? items.OrderByDescending(i => i, comparer) : items.OrderBy(i => i, comparer); @@ -3076,9 +3074,9 @@ namespace Emby.Server.Implementations.Library }); } - public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo) + public void AddMediaPath(string virtualFolderName, MediaPathInfo mediaPath) { - AddMediaPathInternal(virtualFolderName, pathInfo, true); + AddMediaPathInternal(virtualFolderName, mediaPath, true); } private void AddMediaPathInternal(string virtualFolderName, MediaPathInfo pathInfo, bool saveLibraryOptions) @@ -3131,11 +3129,11 @@ namespace Emby.Server.Implementations.Library } } - public void UpdateMediaPath(string virtualFolderName, MediaPathInfo pathInfo) + public void UpdateMediaPath(string virtualFolderName, MediaPathInfo mediaPath) { - if (pathInfo == null) + if (mediaPath == null) { - throw new ArgumentNullException(nameof(pathInfo)); + throw new ArgumentNullException(nameof(mediaPath)); } var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; @@ -3148,9 +3146,9 @@ namespace Emby.Server.Implementations.Library var list = libraryOptions.PathInfos.ToList(); foreach (var originalPathInfo in list) { - if (string.Equals(pathInfo.Path, originalPathInfo.Path, StringComparison.Ordinal)) + if (string.Equals(mediaPath.Path, originalPathInfo.Path, StringComparison.Ordinal)) { - originalPathInfo.NetworkPath = pathInfo.NetworkPath; + originalPathInfo.NetworkPath = mediaPath.NetworkPath; break; } } diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index 06300adebc..e2f1fb0ade 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -36,9 +36,10 @@ namespace Emby.Server.Implementations.Library return list.Concat(GetInstantMixFromGenres(item.Genres, user, dtoOptions)).ToList(); } - public List<BaseItem> GetInstantMixFromArtist(MusicArtist item, User user, DtoOptions dtoOptions) + /// <inheritdoc /> + public List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User user, DtoOptions dtoOptions) { - return GetInstantMixFromGenres(item.Genres, user, dtoOptions); + return GetInstantMixFromGenres(artist.Genres, user, dtoOptions); } public List<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User user, DtoOptions dtoOptions) diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index 01e89302e9..b102b86cfb 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -21,13 +21,13 @@ namespace Emby.Server.Implementations.Library.Resolvers public abstract class BaseVideoResolver<T> : MediaBrowser.Controller.Resolvers.ItemResolver<T> where T : Video, new() { - protected readonly ILibraryManager LibraryManager; - protected BaseVideoResolver(ILibraryManager libraryManager) { LibraryManager = libraryManager; } + protected ILibraryManager LibraryManager { get; } + /// <summary> /// Resolves the specified args. /// </summary> diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index 8aa605a903..c4e230f211 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -177,6 +177,7 @@ namespace Emby.Server.Implementations.Library return dto; } + /// <inheritdoc /> public UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto itemDto, User user, DtoOptions options) { var userData = GetUserData(user, item); @@ -191,7 +192,7 @@ namespace Emby.Server.Implementations.Library /// </summary> /// <param name="data">The data.</param> /// <returns>DtoUserItemData.</returns> - /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentNullException"><paramref name="data"/> is <c>null</c>.</exception> private UserItemDataDto GetUserItemDataDto(UserItemData data) { if (data == null) @@ -212,6 +213,7 @@ namespace Emby.Server.Implementations.Library }; } + /// <inheritdoc /> public bool UpdatePlayState(BaseItem item, UserItemData data, long? reportedPositionTicks) { var playedToCompletion = false; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index f2b9f3cb90..026b6bc0bb 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -610,11 +610,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV throw new NotImplementedException(); } - public Task<string> CreateTimer(TimerInfo timer, CancellationToken cancellationToken) + public Task<string> CreateTimer(TimerInfo info, CancellationToken cancellationToken) { - var existingTimer = string.IsNullOrWhiteSpace(timer.ProgramId) ? + var existingTimer = string.IsNullOrWhiteSpace(info.ProgramId) ? null : - _timerProvider.GetTimerByProgramId(timer.ProgramId); + _timerProvider.GetTimerByProgramId(info.ProgramId); if (existingTimer != null) { @@ -632,32 +632,32 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - timer.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); + info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); LiveTvProgram programInfo = null; - if (!string.IsNullOrWhiteSpace(timer.ProgramId)) + if (!string.IsNullOrWhiteSpace(info.ProgramId)) { - programInfo = GetProgramInfoFromCache(timer); + programInfo = GetProgramInfoFromCache(info); } if (programInfo == null) { - _logger.LogInformation("Unable to find program with Id {0}. Will search using start date", timer.ProgramId); - programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate); + _logger.LogInformation("Unable to find program with Id {0}. Will search using start date", info.ProgramId); + programInfo = GetProgramInfoFromCache(info.ChannelId, info.StartDate); } if (programInfo != null) { - CopyProgramInfoToTimerInfo(programInfo, timer); + CopyProgramInfoToTimerInfo(programInfo, info); } - timer.IsManual = true; - _timerProvider.Add(timer); + info.IsManual = true; + _timerProvider.Add(info); - TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(timer)); + TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(info)); - return Task.FromResult(timer.Id); + return Task.FromResult(info.Id); } public async Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken) diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index ce585d0fb5..ea1a28fe86 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -65,6 +65,8 @@ namespace Emby.Server.Implementations.LiveTv private ITunerHost[] _tunerHosts = Array.Empty<ITunerHost>(); private IListingsProvider[] _listingProviders = Array.Empty<IListingsProvider>(); + private bool _disposed = false; + public LiveTvManager( IServerConfigurationManager config, ILogger<LiveTvManager> logger, @@ -520,7 +522,7 @@ namespace Emby.Server.Implementations.LiveTv return item; } - private Tuple<LiveTvProgram, bool, bool> GetProgram(ProgramInfo info, Dictionary<Guid, LiveTvProgram> allExistingPrograms, LiveTvChannel channel, ChannelType channelType, string serviceName, CancellationToken cancellationToken) + private (LiveTvProgram item, bool isNew, bool isUpdated) GetProgram(ProgramInfo info, Dictionary<Guid, LiveTvProgram> allExistingPrograms, LiveTvChannel channel) { var id = _tvDtoService.GetInternalProgramId(info.Id); @@ -559,8 +561,6 @@ namespace Emby.Server.Implementations.LiveTv item.ParentId = channel.Id; - // item.ChannelType = channelType; - item.Audio = info.Audio; item.ChannelId = channel.Id; item.CommunityRating ??= info.CommunityRating; @@ -772,7 +772,7 @@ namespace Emby.Server.Implementations.LiveTv item.OnMetadataChanged(); } - return new Tuple<LiveTvProgram, bool, bool>(item, isNew, isUpdated); + return (item, isNew, isUpdated); } public async Task<BaseItemDto> GetProgram(string id, CancellationToken cancellationToken, User user = null) @@ -1187,14 +1187,14 @@ namespace Emby.Server.Implementations.LiveTv foreach (var program in channelPrograms) { - var programTuple = GetProgram(program, existingPrograms, currentChannel, currentChannel.ChannelType, service.Name, cancellationToken); - var programItem = programTuple.Item1; + var programTuple = GetProgram(program, existingPrograms, currentChannel); + var programItem = programTuple.item; - if (programTuple.Item2) + if (programTuple.isNew) { newPrograms.Add(programItem); } - else if (programTuple.Item3) + else if (programTuple.isUpdated) { updatedPrograms.Add(programItem); } @@ -1385,10 +1385,10 @@ namespace Emby.Server.Implementations.LiveTv // var items = allActivePaths.Select(i => _libraryManager.FindByPath(i, false)).Where(i => i != null).ToArray(); // return new QueryResult<BaseItem> - //{ + // { // Items = items, // TotalRecordCount = items.Length - //}; + // }; dtoOptions.Fields = dtoOptions.Fields.Concat(new[] { ItemFields.Tags }).Distinct().ToArray(); } @@ -1425,16 +1425,15 @@ namespace Emby.Server.Implementations.LiveTv return result; } - public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> tuples, IReadOnlyList<ItemFields> fields, User user = null) + public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> programs, IReadOnlyList<ItemFields> fields, User user = null) { var programTuples = new List<Tuple<BaseItemDto, string, string>>(); var hasChannelImage = fields.Contains(ItemFields.ChannelImage); var hasChannelInfo = fields.Contains(ItemFields.ChannelInfo); - foreach (var tuple in tuples) + foreach (var (item, dto) in programs) { - var program = (LiveTvProgram)tuple.Item1; - var dto = tuple.Item2; + var program = (LiveTvProgram)item; dto.StartDate = program.StartDate; dto.EpisodeTitle = program.EpisodeTitle; @@ -1871,11 +1870,11 @@ namespace Emby.Server.Implementations.LiveTv return _libraryManager.GetItemById(internalChannelId); } - public void AddChannelInfo(IReadOnlyCollection<(BaseItemDto, LiveTvChannel)> tuples, DtoOptions options, User user) + public void AddChannelInfo(IReadOnlyCollection<(BaseItemDto, LiveTvChannel)> items, DtoOptions options, User user) { var now = DateTime.UtcNow; - var channelIds = tuples.Select(i => i.Item2.Id).Distinct().ToArray(); + var channelIds = items.Select(i => i.Item2.Id).Distinct().ToArray(); var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user) { @@ -1896,7 +1895,7 @@ namespace Emby.Server.Implementations.LiveTv var addCurrentProgram = options.AddCurrentProgram; - foreach (var tuple in tuples) + foreach (var tuple in items) { var dto = tuple.Item1; var channel = tuple.Item2; @@ -2118,17 +2117,13 @@ namespace Emby.Server.Implementations.LiveTv }; } - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> + /// <inheritdoc /> public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } - private bool _disposed = false; - /// <summary> /// Releases unmanaged and - optionally - managed resources. /// </summary> @@ -2324,20 +2319,20 @@ namespace Emby.Server.Implementations.LiveTv _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>(); } - public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelId, string providerChannelId) + public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber) { var config = GetConfiguration(); var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase)); - listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings.Where(i => !string.Equals(i.Name, tunerChannelId, StringComparison.OrdinalIgnoreCase)).ToArray(); + listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings.Where(i => !string.Equals(i.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)).ToArray(); - if (!string.Equals(tunerChannelId, providerChannelId, StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase)) { var list = listingsProviderInfo.ChannelMappings.ToList(); list.Add(new NameValuePair { - Name = tunerChannelId, - Value = providerChannelId + Name = tunerChannelNumber, + Value = providerChannelNumber }); listingsProviderInfo.ChannelMappings = list.ToArray(); } @@ -2357,10 +2352,10 @@ namespace Emby.Server.Implementations.LiveTv _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>(); - return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelId, StringComparison.OrdinalIgnoreCase)); + return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)); } - public TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, List<ChannelInfo> epgChannels) + public TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, List<ChannelInfo> providerChannels) { var result = new TunerChannelMapping { @@ -2373,7 +2368,7 @@ namespace Emby.Server.Implementations.LiveTv result.Name = tunerChannel.Number + " " + result.Name; } - var providerChannel = EmbyTV.EmbyTV.Current.GetEpgChannelFromTunerChannel(mappings, tunerChannel, epgChannels); + var providerChannel = EmbyTV.EmbyTV.Current.GetEpgChannelFromTunerChannel(mappings, tunerChannel, providerChannels); if (providerChannel != null) { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs index 5941613cf9..9bff0861bb 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs @@ -158,7 +158,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return new List<MediaSourceInfo>(); } - protected abstract Task<ILiveStream> GetChannelStream(TunerHostInfo tuner, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken); + protected abstract Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken); public async Task<ILiveStream> GetChannelStream(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 011748d1d6..2bd12a9c8f 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -95,17 +95,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun public bool IsLegacyTuner { get; set; } } - protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken) + protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken) { - var lineup = await GetLineup(info, cancellationToken).ConfigureAwait(false); + var lineup = await GetLineup(tuner, cancellationToken).ConfigureAwait(false); return lineup.Select(i => new HdHomerunChannelInfo { Name = i.GuideName, Number = i.GuideNumber, - Id = GetChannelId(info, i), + Id = GetChannelId(tuner, i), IsFavorite = i.Favorite, - TunerHostId = info.Id, + TunerHostId = tuner.Id, IsHD = i.HD, AudioCodec = i.AudioCodec, VideoCodec = i.VideoCodec, @@ -496,57 +496,53 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun return mediaSource; } - protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, ChannelInfo channelInfo, CancellationToken cancellationToken) + protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, ChannelInfo channel, CancellationToken cancellationToken) { var list = new List<MediaSourceInfo>(); - var channelId = channelInfo.Id; + var channelId = channel.Id; var hdhrId = GetHdHrIdFromChannelId(channelId); - var hdHomerunChannelInfo = channelInfo as HdHomerunChannelInfo; - - var isLegacyTuner = hdHomerunChannelInfo != null && hdHomerunChannelInfo.IsLegacyTuner; - - if (isLegacyTuner) + if (channel is HdHomerunChannelInfo hdHomerunChannelInfo && hdHomerunChannelInfo.IsLegacyTuner) { - list.Add(GetMediaSource(info, hdhrId, channelInfo, "native")); + list.Add(GetMediaSource(tuner, hdhrId, channel, "native")); } else { - var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); + var modelInfo = await GetModelInfo(tuner, false, cancellationToken).ConfigureAwait(false); if (modelInfo != null && modelInfo.SupportsTranscoding) { - if (info.AllowHWTranscoding) + if (tuner.AllowHWTranscoding) { - list.Add(GetMediaSource(info, hdhrId, channelInfo, "heavy")); + list.Add(GetMediaSource(tuner, hdhrId, channel, "heavy")); - list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet540")); - list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet480")); - list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet360")); - list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet240")); - list.Add(GetMediaSource(info, hdhrId, channelInfo, "mobile")); + list.Add(GetMediaSource(tuner, hdhrId, channel, "internet540")); + list.Add(GetMediaSource(tuner, hdhrId, channel, "internet480")); + list.Add(GetMediaSource(tuner, hdhrId, channel, "internet360")); + list.Add(GetMediaSource(tuner, hdhrId, channel, "internet240")); + list.Add(GetMediaSource(tuner, hdhrId, channel, "mobile")); } - list.Add(GetMediaSource(info, hdhrId, channelInfo, "native")); + list.Add(GetMediaSource(tuner, hdhrId, channel, "native")); } if (list.Count == 0) { - list.Add(GetMediaSource(info, hdhrId, channelInfo, "native")); + list.Add(GetMediaSource(tuner, hdhrId, channel, "native")); } } return list; } - protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) + protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) { - var tunerCount = info.TunerCount; + var tunerCount = tunerHost.TunerCount; if (tunerCount > 0) { - var tunerHostId = info.Id; + var tunerHostId = tunerHost.Id; var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase)); if (liveStreams.Count() >= tunerCount) @@ -557,26 +553,26 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var profile = streamId.Split('_')[0]; - Logger.LogInformation("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channelInfo.Id, streamId, profile); + Logger.LogInformation("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channel.Id, streamId, profile); - var hdhrId = GetHdHrIdFromChannelId(channelInfo.Id); + var hdhrId = GetHdHrIdFromChannelId(channel.Id); - var hdhomerunChannel = channelInfo as HdHomerunChannelInfo; + var hdhomerunChannel = channel as HdHomerunChannelInfo; - var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); + var modelInfo = await GetModelInfo(tunerHost, false, cancellationToken).ConfigureAwait(false); if (!modelInfo.SupportsTranscoding) { profile = "native"; } - var mediaSource = GetMediaSource(info, hdhrId, channelInfo, profile); + var mediaSource = GetMediaSource(tunerHost, hdhrId, channel, profile); if (hdhomerunChannel != null && hdhomerunChannel.IsLegacyTuner) { return new HdHomerunUdpStream( mediaSource, - info, + tunerHost, streamId, new LegacyHdHomerunChannelCommands(hdhomerunChannel.Path), modelInfo.TunerCount, @@ -592,7 +588,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { mediaSource.Protocol = MediaProtocol.Http; - var httpUrl = channelInfo.Path; + var httpUrl = channel.Path; // If raw was used, the tuner doesn't support params if (!string.IsNullOrWhiteSpace(profile) && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase)) @@ -604,7 +600,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun return new SharedHttpStream( mediaSource, - info, + tunerHost, streamId, FileSystem, _httpClientFactory, @@ -616,7 +612,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun return new HdHomerunUdpStream( mediaSource, - info, + tunerHost, streamId, new HdHomerunChannelCommands(hdhomerunChannel.Number, profile), modelInfo.TunerCount, diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 8fa6f5ad68..08b9260b98 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -71,12 +71,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return ChannelIdPrefix + info.Url.GetMD5().ToString("N", CultureInfo.InvariantCulture); } - protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken) + protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken) { - var channelIdPrefix = GetFullChannelIdPrefix(info); + var channelIdPrefix = GetFullChannelIdPrefix(tuner); return await new M3uParser(Logger, _httpClientFactory) - .Parse(info, channelIdPrefix, cancellationToken) + .Parse(tuner, channelIdPrefix, cancellationToken) .ConfigureAwait(false); } @@ -96,13 +96,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return Task.FromResult(list); } - protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) + protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) { - var tunerCount = info.TunerCount; + var tunerCount = tunerHost.TunerCount; if (tunerCount > 0) { - var tunerHostId = info.Id; + var tunerHostId = tunerHost.Id; var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase)); if (liveStreams.Count() >= tunerCount) @@ -111,7 +111,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts } } - var sources = await GetChannelStreamMediaSources(info, channelInfo, cancellationToken).ConfigureAwait(false); + var sources = await GetChannelStreamMediaSources(tunerHost, channel, cancellationToken).ConfigureAwait(false); var mediaSource = sources[0]; @@ -121,11 +121,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) { - return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper); + return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper); } } - return new LiveStream(mediaSource, info, FileSystem, Logger, Config, _streamHelper); + return new LiveStream(mediaSource, tunerHost, FileSystem, Logger, Config, _streamHelper); } public async Task Validate(TunerHostInfo info) @@ -135,9 +135,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts } } - protected override Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, ChannelInfo channelInfo, CancellationToken cancellationToken) + protected override Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, ChannelInfo channel, CancellationToken cancellationToken) { - return Task.FromResult(new List<MediaSourceInfo> { CreateMediaSourceInfo(info, channelInfo) }); + return Task.FromResult(new List<MediaSourceInfo> { CreateMediaSourceInfo(tuner, channel) }); } protected virtual MediaSourceInfo CreateMediaSourceInfo(TunerHostInfo info, ChannelInfo channel) diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs index 1377286168..6d0c8731e9 100644 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ b/Emby.Server.Implementations/Net/SocketFactory.cs @@ -11,6 +11,7 @@ namespace Emby.Server.Implementations.Net { public class SocketFactory : ISocketFactory { + /// <inheritdoc /> public ISocket CreateUdpBroadcastSocket(int localPort) { if (localPort < 0) @@ -35,11 +36,8 @@ namespace Emby.Server.Implementations.Net } } - /// <summary> - /// Creates a new UDP acceptSocket that is a member of the SSDP multicast local admin group and binds it to the specified local port. - /// </summary> - /// <returns>An implementation of the <see cref="ISocket"/> interface used by RSSDP components to perform acceptSocket operations.</returns> - public ISocket CreateSsdpUdpSocket(IPAddress localIpAddress, int localPort) + /// <inheritdoc /> + public ISocket CreateSsdpUdpSocket(IPAddress localIp, int localPort) { if (localPort < 0) { @@ -53,8 +51,8 @@ namespace Emby.Server.Implementations.Net retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 4); - retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse("239.255.255.250"), localIpAddress)); - return new UdpSocket(retVal, localPort, localIpAddress); + retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse("239.255.255.250"), localIp)); + return new UdpSocket(retVal, localPort, localIp); } catch { @@ -64,13 +62,7 @@ namespace Emby.Server.Implementations.Net } } - /// <summary> - /// Creates a new UDP acceptSocket that is a member of the specified multicast IP address, and binds it to the specified local port. - /// </summary> - /// <param name="ipAddress">The multicast IP address to make the acceptSocket a member of.</param> - /// <param name="multicastTimeToLive">The multicast time to live value for the acceptSocket.</param> - /// <param name="localPort">The number of the local port to bind to.</param> - /// <returns></returns> + /// <inheritdoc /> public ISocket CreateUdpMulticastSocket(string ipAddress, int multicastTimeToLive, int localPort) { if (ipAddress == null) diff --git a/Emby.Server.Implementations/Net/UdpSocket.cs b/Emby.Server.Implementations/Net/UdpSocket.cs index a8b18d2925..9b799e854d 100644 --- a/Emby.Server.Implementations/Net/UdpSocket.cs +++ b/Emby.Server.Implementations/Net/UdpSocket.cs @@ -191,7 +191,7 @@ namespace Emby.Server.Implementations.Net return taskCompletion.Task; } - public Task SendToAsync(byte[] buffer, int offset, int size, IPEndPoint endPoint, CancellationToken cancellationToken) + public Task SendToAsync(byte[] buffer, int offset, int bytes, IPEndPoint endPoint, CancellationToken cancellationToken) { ThrowIfDisposed(); @@ -214,7 +214,7 @@ namespace Emby.Server.Implementations.Net } }; - var result = BeginSendTo(buffer, offset, size, endPoint, new AsyncCallback(callback), null); + var result = BeginSendTo(buffer, offset, bytes, endPoint, new AsyncCallback(callback), null); if (result.CompletedSynchronously) { diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs index 692d1667d2..a575b260cb 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs @@ -29,6 +29,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// <summary> /// Initializes a new instance of the <see cref="DeleteCacheFileTask" /> class. /// </summary> + /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param> + /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> + /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> + /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> public DeleteCacheFileTask( IApplicationPaths appPaths, ILogger<DeleteCacheFileTask> logger, diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index ea710013e9..4111590c83 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -235,12 +235,12 @@ namespace Emby.Server.Implementations.Session } /// <inheritdoc /> - public void UpdateDeviceName(string sessionId, string deviceName) + public void UpdateDeviceName(string sessionId, string reportedDeviceName) { var session = GetSession(sessionId); if (session != null) { - session.DeviceName = deviceName; + session.DeviceName = reportedDeviceName; } } @@ -316,14 +316,14 @@ namespace Emby.Server.Implementations.Session } /// <inheritdoc /> - public void OnSessionControllerConnected(SessionInfo info) + public void OnSessionControllerConnected(SessionInfo session) { EventHelper.QueueEventIfNotNull( SessionControllerConnected, this, new SessionEventArgs { - SessionInfo = info + SessionInfo = session }, _logger); } @@ -1581,16 +1581,16 @@ namespace Emby.Server.Implementations.Session } /// <inheritdoc /> - public async Task Logout(Device existing) + public async Task Logout(Device device) { CheckDisposed(); - _logger.LogInformation("Logging out access token {0}", existing.AccessToken); + _logger.LogInformation("Logging out access token {0}", device.AccessToken); - await _deviceManager.DeleteDevice(existing).ConfigureAwait(false); + await _deviceManager.DeleteDevice(device).ConfigureAwait(false); var sessions = Sessions - .Where(i => string.Equals(i.DeviceId, existing.DeviceId, StringComparison.OrdinalIgnoreCase)) + .Where(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase)) .ToList(); foreach (var session in sessions) @@ -1601,7 +1601,7 @@ namespace Emby.Server.Implementations.Session } catch (Exception ex) { - _logger.LogError("Error reporting session ended", ex); + _logger.LogError(ex, "Error reporting session ended"); } } } diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index af453d1480..4d990c8718 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -33,9 +33,9 @@ namespace Emby.Server.Implementations.TV _configurationManager = configurationManager; } - public QueryResult<BaseItem> GetNextUp(NextUpQuery request, DtoOptions dtoOptions) + public QueryResult<BaseItem> GetNextUp(NextUpQuery query, DtoOptions options) { - var user = _userManager.GetUserById(request.UserId); + var user = _userManager.GetUserById(query.UserId); if (user == null) { @@ -43,9 +43,9 @@ namespace Emby.Server.Implementations.TV } string presentationUniqueKey = null; - if (!string.IsNullOrEmpty(request.SeriesId)) + if (!string.IsNullOrEmpty(query.SeriesId)) { - if (_libraryManager.GetItemById(request.SeriesId) is Series series) + if (_libraryManager.GetItemById(query.SeriesId) is Series series) { presentationUniqueKey = GetUniqueSeriesKey(series); } @@ -53,14 +53,14 @@ namespace Emby.Server.Implementations.TV if (!string.IsNullOrEmpty(presentationUniqueKey)) { - return GetResult(GetNextUpEpisodes(request, user, new[] { presentationUniqueKey }, dtoOptions), request); + return GetResult(GetNextUpEpisodes(query, user, new[] { presentationUniqueKey }, options), query); } BaseItem[] parents; - if (request.ParentId.HasValue) + if (query.ParentId.HasValue) { - var parent = _libraryManager.GetItemById(request.ParentId.Value); + var parent = _libraryManager.GetItemById(query.ParentId.Value); if (parent != null) { @@ -79,10 +79,10 @@ namespace Emby.Server.Implementations.TV .ToArray(); } - return GetNextUp(request, parents, dtoOptions); + return GetNextUp(query, parents, options); } - public QueryResult<BaseItem> GetNextUp(NextUpQuery request, BaseItem[] parentsFolders, DtoOptions dtoOptions) + public QueryResult<BaseItem> GetNextUp(NextUpQuery request, BaseItem[] parentsFolders, DtoOptions options) { var user = _userManager.GetUserById(request.UserId); @@ -104,7 +104,7 @@ namespace Emby.Server.Implementations.TV if (!string.IsNullOrEmpty(presentationUniqueKey)) { - return GetResult(GetNextUpEpisodes(request, user, new[] { presentationUniqueKey }, dtoOptions), request); + return GetResult(GetNextUpEpisodes(request, user, new[] { presentationUniqueKey }, options), request); } if (limit.HasValue) @@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.TV .Select(GetUniqueSeriesKey); // Avoid implicitly captured closure - var episodes = GetNextUpEpisodes(request, user, items, dtoOptions); + var episodes = GetNextUpEpisodes(request, user, items, options); return GetResult(episodes, request); } diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index 0fb4771dd3..7dc7f774d0 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -97,8 +97,7 @@ namespace MediaBrowser.Controller.Entities { try { - var result = XmlSerializer.DeserializeFromFile(typeof(LibraryOptions), GetLibraryOptionsPath(path)) as LibraryOptions; - if (result == null) + if (XmlSerializer.DeserializeFromFile(typeof(LibraryOptions), GetLibraryOptionsPath(path)) is not LibraryOptions result) { return new LibraryOptions(); } diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 753c18bc7e..b0abca367c 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -104,13 +104,6 @@ namespace MediaBrowser.Controller /// <returns>The API URL.</returns> string GetLocalApiUrl(string hostname, string scheme = null, int? port = null); - /// <summary> - /// Open a URL in an external browser window. - /// </summary> - /// <param name="url">The URL to open.</param> - /// <exception cref="NotSupportedException"><see cref="CanLaunchWebBrowser"/> is false.</exception> - void LaunchUrl(string url); - IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo(); string ExpandVirtualPath(string path); diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 604960d8bc..d40e56c7d9 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -595,11 +595,11 @@ namespace MediaBrowser.Controller.Library Task RemoveVirtualFolder(string name, bool refreshLibrary); - void AddMediaPath(string virtualFolderName, MediaPathInfo path); + void AddMediaPath(string virtualFolderName, MediaPathInfo mediaPath); - void UpdateMediaPath(string virtualFolderName, MediaPathInfo path); + void UpdateMediaPath(string virtualFolderName, MediaPathInfo mediaPath); - void RemoveMediaPath(string virtualFolderName, string path); + void RemoveMediaPath(string virtualFolderName, string mediaPath); QueryResult<(BaseItem, ItemCounts)> GetGenres(InternalItemsQuery query); diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs index cf35b48dba..034c405910 100644 --- a/MediaBrowser.Controller/Library/IUserDataManager.cs +++ b/MediaBrowser.Controller/Library/IUserDataManager.cs @@ -47,7 +47,7 @@ namespace MediaBrowser.Controller.Library /// <returns>User data dto.</returns> UserItemDataDto GetUserDataDto(BaseItem item, User user); - UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto itemDto, User user, DtoOptions dto_options); + UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto itemDto, User user, DtoOptions options); /// <summary> /// Get all user data for the given user. @@ -69,8 +69,8 @@ namespace MediaBrowser.Controller.Library /// </summary> /// <param name="item">Item to update.</param> /// <param name="data">Data to update.</param> - /// <param name="positionTicks">New playstate.</param> + /// <param name="reportedPositionTicks">New playstate.</param> /// <returns>True if playstate was updated.</returns> - bool UpdatePlayState(BaseItem item, UserItemData data, long? positionTicks); + bool UpdatePlayState(BaseItem item, UserItemData data, long? reportedPositionTicks); } } diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index bd097c90ac..dbd18165db 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -274,7 +274,7 @@ namespace MediaBrowser.Controller.LiveTv Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber); - TunerChannelMapping GetTunerChannelMapping(ChannelInfo channel, NameValuePair[] mappings, List<ChannelInfo> providerChannels); + TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, List<ChannelInfo> providerChannels); /// <summary> /// Gets the lineups. diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs index 897f263f3d..ce34954e3f 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs @@ -70,10 +70,10 @@ namespace MediaBrowser.Controller.LiveTv /// <summary> /// Updates the timer asynchronous. /// </summary> - /// <param name="info">The information.</param> + /// <param name="updatedTimer">The updated timer information.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - Task UpdateTimerAsync(TimerInfo info, CancellationToken cancellationToken); + Task UpdateTimerAsync(TimerInfo updatedTimer, CancellationToken cancellationToken); /// <summary> /// Updates the series timer asynchronous. diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 0a9073e7f5..a084f91969 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -49,17 +49,17 @@ namespace MediaBrowser.Controller.Persistence /// <summary> /// Gets chapters for an item. /// </summary> - /// <param name="id">The item.</param> + /// <param name="item">The item.</param> /// <returns>The list of chapter info.</returns> - List<ChapterInfo> GetChapters(BaseItem id); + List<ChapterInfo> GetChapters(BaseItem item); /// <summary> /// Gets a single chapter for an item. /// </summary> - /// <param name="id">The item.</param> + /// <param name="item">The item.</param> /// <param name="index">The chapter index.</param> /// <returns>The chapter info at the specified index.</returns> - ChapterInfo GetChapter(BaseItem id, int index); + ChapterInfo GetChapter(BaseItem item, int index); /// <summary> /// Saves the chapters. diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 23b36cc10e..cc12cb102b 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -344,7 +344,7 @@ namespace MediaBrowser.Controller.Session /// <returns>A <see cref="Task"/> representing the log out process.</returns> Task Logout(string accessToken); - Task Logout(Device accessToken); + Task Logout(Device device); /// <summary> /// Revokes the user tokens. diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 432c1c1d60..4414415a27 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -133,7 +133,7 @@ namespace MediaBrowser.Model.Dlna var stream = TargetAudioStream; return AudioSampleRate.HasValue && !IsDirectStream ? AudioSampleRate - : stream == null ? null : stream.SampleRate; + : stream?.SampleRate; } } @@ -146,7 +146,7 @@ namespace MediaBrowser.Model.Dlna { if (IsDirectStream) { - return TargetAudioStream == null ? (int?)null : TargetAudioStream.BitDepth; + return TargetAudioStream?.BitDepth; } var targetAudioCodecs = TargetAudioCodec; @@ -156,7 +156,7 @@ namespace MediaBrowser.Model.Dlna return GetTargetAudioBitDepth(audioCodec); } - return TargetAudioStream == null ? (int?)null : TargetAudioStream.BitDepth; + return TargetAudioStream?.BitDepth; } } @@ -169,7 +169,7 @@ namespace MediaBrowser.Model.Dlna { if (IsDirectStream) { - return TargetVideoStream == null ? (int?)null : TargetVideoStream.BitDepth; + return TargetVideoStream?.BitDepth; } var targetVideoCodecs = TargetVideoCodec; @@ -179,7 +179,7 @@ namespace MediaBrowser.Model.Dlna return GetTargetVideoBitDepth(videoCodec); } - return TargetVideoStream == null ? (int?)null : TargetVideoStream.BitDepth; + return TargetVideoStream?.BitDepth; } } @@ -193,7 +193,7 @@ namespace MediaBrowser.Model.Dlna { if (IsDirectStream) { - return TargetVideoStream == null ? (int?)null : TargetVideoStream.RefFrames; + return TargetVideoStream?.RefFrames; } var targetVideoCodecs = TargetVideoCodec; @@ -203,7 +203,7 @@ namespace MediaBrowser.Model.Dlna return GetTargetRefFrames(videoCodec); } - return TargetVideoStream == null ? (int?)null : TargetVideoStream.RefFrames; + return TargetVideoStream?.RefFrames; } } @@ -230,7 +230,7 @@ namespace MediaBrowser.Model.Dlna { if (IsDirectStream) { - return TargetVideoStream == null ? (double?)null : TargetVideoStream.Level; + return TargetVideoStream?.Level; } var targetVideoCodecs = TargetVideoCodec; @@ -240,7 +240,7 @@ namespace MediaBrowser.Model.Dlna return GetTargetVideoLevel(videoCodec); } - return TargetVideoStream == null ? (double?)null : TargetVideoStream.Level; + return TargetVideoStream?.Level; } } @@ -254,7 +254,7 @@ namespace MediaBrowser.Model.Dlna var stream = TargetVideoStream; return !IsDirectStream ? null - : stream == null ? null : stream.PacketLength; + : stream?.PacketLength; } } @@ -267,7 +267,7 @@ namespace MediaBrowser.Model.Dlna { if (IsDirectStream) { - return TargetVideoStream == null ? null : TargetVideoStream.Profile; + return TargetVideoStream?.Profile; } var targetVideoCodecs = TargetVideoCodec; @@ -277,7 +277,7 @@ namespace MediaBrowser.Model.Dlna return GetOption(videoCodec, "profile"); } - return TargetVideoStream == null ? null : TargetVideoStream.Profile; + return TargetVideoStream?.Profile; } } @@ -292,7 +292,7 @@ namespace MediaBrowser.Model.Dlna var stream = TargetVideoStream; return !IsDirectStream ? null - : stream == null ? null : stream.CodecTag; + : stream?.CodecTag; } } @@ -306,7 +306,7 @@ namespace MediaBrowser.Model.Dlna var stream = TargetAudioStream; return AudioBitrate.HasValue && !IsDirectStream ? AudioBitrate - : stream == null ? null : stream.BitRate; + : stream?.BitRate; } } @@ -319,7 +319,7 @@ namespace MediaBrowser.Model.Dlna { if (IsDirectStream) { - return TargetAudioStream == null ? (int?)null : TargetAudioStream.Channels; + return TargetAudioStream?.Channels; } var targetAudioCodecs = TargetAudioCodec; @@ -329,7 +329,7 @@ namespace MediaBrowser.Model.Dlna return GetTargetRefFrames(codec); } - return TargetAudioStream == null ? (int?)null : TargetAudioStream.Channels; + return TargetAudioStream?.Channels; } } @@ -425,7 +425,7 @@ namespace MediaBrowser.Model.Dlna return VideoBitrate.HasValue && !IsDirectStream ? VideoBitrate - : stream == null ? null : stream.BitRate; + : stream?.BitRate; } } @@ -451,7 +451,7 @@ namespace MediaBrowser.Model.Dlna { if (IsDirectStream) { - return TargetVideoStream == null ? null : TargetVideoStream.IsAnamorphic; + return TargetVideoStream?.IsAnamorphic; } return false; @@ -464,7 +464,7 @@ namespace MediaBrowser.Model.Dlna { if (IsDirectStream) { - return TargetVideoStream == null ? (bool?)null : TargetVideoStream.IsInterlaced; + return TargetVideoStream?.IsInterlaced; } var targetVideoCodecs = TargetVideoCodec; @@ -477,7 +477,7 @@ namespace MediaBrowser.Model.Dlna } } - return TargetVideoStream == null ? (bool?)null : TargetVideoStream.IsInterlaced; + return TargetVideoStream?.IsInterlaced; } } @@ -487,7 +487,7 @@ namespace MediaBrowser.Model.Dlna { if (IsDirectStream) { - return TargetVideoStream == null ? null : TargetVideoStream.IsAVC; + return TargetVideoStream?.IsAVC; } return true; @@ -618,20 +618,20 @@ namespace MediaBrowser.Model.Dlna } // Try to keep the url clean by omitting defaults - if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase) && - string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase) + && string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase)) { continue; } - if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase) && - string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase) + && string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase)) { continue; } - if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase) && - string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase) + && string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase)) { continue; } @@ -641,7 +641,7 @@ namespace MediaBrowser.Model.Dlna list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue)); } - string queryString = string.Join("&", list.ToArray()); + string queryString = string.Join('&', list); return GetUrl(baseUrl, queryString); } @@ -681,11 +681,11 @@ namespace MediaBrowser.Model.Dlna string audioCodecs = item.AudioCodecs.Length == 0 ? string.Empty : - string.Join(",", item.AudioCodecs); + string.Join(',', item.AudioCodecs); string videoCodecs = item.VideoCodecs.Length == 0 ? string.Empty : - string.Join(",", item.VideoCodecs); + string.Join(',', item.VideoCodecs); list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty)); list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty)); @@ -1024,30 +1024,5 @@ namespace MediaBrowser.Model.Dlna return count; } - - public List<MediaStream> GetSelectableAudioStreams() - { - return GetSelectableStreams(MediaStreamType.Audio); - } - - public List<MediaStream> GetSelectableSubtitleStreams() - { - return GetSelectableStreams(MediaStreamType.Subtitle); - } - - public List<MediaStream> GetSelectableStreams(MediaStreamType type) - { - var list = new List<MediaStream>(); - - foreach (var stream in MediaSource.MediaStreams) - { - if (type == stream.Type) - { - list.Add(stream); - } - } - - return list; - } } } diff --git a/jellyfin.ruleset b/jellyfin.ruleset index a2fc7bc8da..68fb9064e4 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -12,6 +12,8 @@ <Rule Id="SA1101" Action="None" /> <!-- disable warning SA1108: Block statements should not contain embedded comments --> <Rule Id="SA1108" Action="None" /> + <!-- disable warning SA1118: Parameter must not span multiple lines. --> + <Rule Id="SA1118" Action="None" /> <!-- disable warning SA1128:: Put constructor initializers on their own line --> <Rule Id="SA1128" Action="None" /> <!-- disable warning SA1130: Use lambda syntax --> @@ -39,6 +41,10 @@ </Rules> <Rules AnalyzerId="Microsoft.CodeAnalysis.NetAnalyzers" RuleNamespace="Microsoft.Design"> + <!-- error on CA1305: Specify IFormatProvider --> + <Rule Id="CA1305" Action="Error" /> + <!-- error on CA1725: Parameter names should match base declaration --> + <Rule Id="CA1725" Action="Error" /> <!-- error on CA2016: Forward the CancellationToken parameter to methods that take one or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token --> <Rule Id="CA2016" Action="Error" />