From 79a7815be7ffaeb0c93cc38fe454b5a0bc5df3c2 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 17 Jan 2023 18:49:00 -0500 Subject: [PATCH 01/69] Use one AssemblyLoadContext per plugin --- Emby.Server.Implementations/Plugins/PluginManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index f2212f4dcb..15ed081572 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -123,14 +123,14 @@ namespace Emby.Server.Implementations.Plugins continue; } + var assemblyLoadContext = new PluginLoadContext(plugin.Path); + _assemblyLoadContexts.Add(assemblyLoadContext); + foreach (var file in plugin.DllFiles) { Assembly assembly; try { - var assemblyLoadContext = new PluginLoadContext(file); - _assemblyLoadContexts.Add(assemblyLoadContext); - assembly = assemblyLoadContext.LoadFromAssemblyPath(file); // Load all required types to verify that the plugin will load From 8cabac0cf24e9b94f95e6118ef994c9346df7efc Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Wed, 18 Jan 2023 10:26:39 -0500 Subject: [PATCH 02/69] Load all plugin assemblies before attempting to load types --- .../Plugins/PluginManager.cs | 55 +++++++++++++------ 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 15ed081572..7c23254a12 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -126,38 +126,61 @@ namespace Emby.Server.Implementations.Plugins var assemblyLoadContext = new PluginLoadContext(plugin.Path); _assemblyLoadContexts.Add(assemblyLoadContext); + var assemblies = new List(plugin.DllFiles.Count); + var loadedAll = true; + foreach (var file in plugin.DllFiles) { - Assembly assembly; try { - assembly = assemblyLoadContext.LoadFromAssemblyPath(file); - - // Load all required types to verify that the plugin will load - assembly.GetTypes(); + assemblies.Add(assemblyLoadContext.LoadFromAssemblyPath(file)); } catch (FileLoadException ex) { - _logger.LogError(ex, "Failed to load assembly {Path}. Disabling plugin.", file); + _logger.LogError(ex, "Failed to load assembly {Path}. Disabling plugin", file); ChangePluginState(plugin, PluginStatus.Malfunctioned); - continue; - } - catch (SystemException ex) when (ex is TypeLoadException or ReflectionTypeLoadException) // Undocumented exception - { - _logger.LogError(ex, "Failed to load assembly {Path}. This error occurs when a plugin references an incompatible version of one of the shared libraries. Disabling plugin.", file); - ChangePluginState(plugin, PluginStatus.NotSupported); - continue; + loadedAll = false; + break; } #pragma warning disable CA1031 // Do not catch general exception types catch (Exception ex) #pragma warning restore CA1031 // Do not catch general exception types { - _logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin.", file); + _logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin", file); ChangePluginState(plugin, PluginStatus.Malfunctioned); - continue; + loadedAll = false; + break; + } + } + + if (!loadedAll) + { + continue; + } + + foreach (var assembly in assemblies) + { + try + { + // Load all required types to verify that the plugin will load + assembly.GetTypes(); + } + catch (SystemException ex) when (ex is TypeLoadException or ReflectionTypeLoadException) // Undocumented exception + { + _logger.LogError(ex, "Failed to load assembly {Path}. This error occurs when a plugin references an incompatible version of one of the shared libraries. Disabling plugin", assembly.Location); + ChangePluginState(plugin, PluginStatus.NotSupported); + break; + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + _logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin", assembly.Location); + ChangePluginState(plugin, PluginStatus.Malfunctioned); + break; } - _logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, file); + _logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, assembly.Location); yield return assembly; } } From 7fa6d4c81e9d1f22fd67a7cab2d70cba27d89275 Mon Sep 17 00:00:00 2001 From: Jpuc1143 Date: Thu, 19 Jan 2023 23:28:52 -0300 Subject: [PATCH 03/69] Add "Allowed Tags" to Parental Controls --- CONTRIBUTORS.md | 1 + .../Data/SqliteItemRepository.cs | 18 ++++++++++++++++++ Jellyfin.Data/Enums/PreferenceKind.cs | 7 ++++++- .../Users/UserManager.cs | 2 ++ .../Migrations/Routines/MigrateUserDb.cs | 1 + MediaBrowser.Controller/Entities/BaseItem.cs | 5 +++++ .../Entities/InternalItemsQuery.cs | 4 ++++ MediaBrowser.Model/Users/UserPolicy.cs | 3 +++ 8 files changed, 40 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index ec3c6fd2af..74c9dbdaf0 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -231,3 +231,4 @@ - [Matthew Jones](https://github.com/matthew-jones-uk) - [Jakob Kukla](https://github.com/jakobkukla) - [Utku Özdemir](https://github.com/utkuozdemir) + - [JPUC1143](https://github.com/Jpuc1143/) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index bc703fe90d..e828bfabfb 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -4477,6 +4477,24 @@ namespace Emby.Server.Implementations.Data } } + if (query.IncludeInheritedTags.Length > 0) + { + var paramName = "@IncludeInheritedTags"; + if (statement is null) + { + int index = 0; + string includedTags = string.Join(',', query.IncludeInheritedTags.Select(_ => paramName + index++)); + whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + includedTags + ")) is not null)"); + } + else + { + for (int index = 0; index < query.IncludeInheritedTags.Length; index++) + { + statement.TryBind(paramName + index, GetCleanValue(query.IncludeInheritedTags[index])); + } + } + } + if (query.SeriesStatuses.Length > 0) { var statuses = new List(); diff --git a/Jellyfin.Data/Enums/PreferenceKind.cs b/Jellyfin.Data/Enums/PreferenceKind.cs index a54d789afb..d2b412e459 100644 --- a/Jellyfin.Data/Enums/PreferenceKind.cs +++ b/Jellyfin.Data/Enums/PreferenceKind.cs @@ -63,6 +63,11 @@ namespace Jellyfin.Data.Enums /// /// A list of ordered views. /// - OrderedViews = 11 + OrderedViews = 11, + + /// + /// A list of allowed tags. + /// + AllowedTags = 12 } } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index dc9d78857e..f9679510d7 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -371,6 +371,7 @@ namespace Jellyfin.Server.Implementations.Users EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing), AccessSchedules = user.AccessSchedules.ToArray(), BlockedTags = user.GetPreference(PreferenceKind.BlockedTags), + AllowedTags = user.GetPreference(PreferenceKind.AllowedTags), EnabledChannels = user.GetPreferenceValues(PreferenceKind.EnabledChannels), EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices), EnabledFolders = user.GetPreferenceValues(PreferenceKind.EnabledFolders), @@ -696,6 +697,7 @@ namespace Jellyfin.Server.Implementations.Users // TODO: fix this at some point user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty()); user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags); + user.SetPreference(PreferenceKind.AllowedTags, policy.AllowedTags); user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels); user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices); user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index ea2f033027..1efd071ec5 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -170,6 +170,7 @@ namespace Jellyfin.Server.Migrations.Routines } user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags); + user.SetPreference(PreferenceKind.AllowedTags, policy.AllowedTags); user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels); user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices); user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders); diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index f2c2007f7a..0cd9720549 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1607,6 +1607,11 @@ namespace MediaBrowser.Controller.Entities return false; } + if (user.GetPreference(PreferenceKind.AllowedTags).Any() && !user.GetPreference(PreferenceKind.AllowedTags).Any(i => Tags.Contains(i, StringComparison.OrdinalIgnoreCase))) + { + return false; + } + return true; } diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index a1e5319048..a51299284b 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -26,6 +26,7 @@ namespace MediaBrowser.Controller.Entities EnableTotalRecordCount = true; ExcludeArtistIds = Array.Empty(); ExcludeInheritedTags = Array.Empty(); + IncludeInheritedTags = Array.Empty(); ExcludeItemIds = Array.Empty(); ExcludeItemTypes = Array.Empty(); ExcludeTags = Array.Empty(); @@ -95,6 +96,8 @@ namespace MediaBrowser.Controller.Entities public string[] ExcludeInheritedTags { get; set; } + public string[] IncludeInheritedTags { get; set; } + public IReadOnlyList Genres { get; set; } public bool? IsSpecialSeason { get; set; } @@ -368,6 +371,7 @@ namespace MediaBrowser.Controller.Entities } ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags); + IncludeInheritedTags = user.GetPreference(PreferenceKind.AllowedTags); User = user; } diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 3634d07058..1619dac5ad 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -35,6 +35,7 @@ namespace MediaBrowser.Model.Users EnableSharedDeviceControl = true; BlockedTags = Array.Empty(); + AllowedTags = Array.Empty(); BlockUnratedItems = Array.Empty(); EnableUserPreferenceAccess = true; @@ -86,6 +87,8 @@ namespace MediaBrowser.Model.Users public string[] BlockedTags { get; set; } + public string[] AllowedTags { get; set; } + public bool EnableUserPreferenceAccess { get; set; } public AccessSchedule[] AccessSchedules { get; set; } From 52230d1c30b76f34132c8c3ad21a09deea72d9d8 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sat, 4 Feb 2023 17:56:12 +0100 Subject: [PATCH 04/69] Return NotFound when itemId isn't found --- .../SyncPlayAccessHandler.cs | 5 + Jellyfin.Api/Controllers/ImageController.cs | 14 +- Jellyfin.Api/Controllers/ItemsController.cs | 5 + Jellyfin.Api/Controllers/LibraryController.cs | 4 + Jellyfin.Api/Controllers/LiveTvController.cs | 2 +- .../Controllers/MusicGenresController.cs | 5 + .../Controllers/PlaystateController.cs | 20 +++ Jellyfin.Api/Controllers/SessionController.cs | 4 + Jellyfin.Api/Controllers/UserController.cs | 25 +++- .../Controllers/UserLibraryController.cs | 41 ++++++ Jellyfin.Api/Controllers/VideosController.cs | 7 +- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 2 +- Jellyfin.Api/Helpers/RequestHelpers.cs | 7 +- .../Models/UserDtos/CreateUserByName.cs | 7 +- .../Models/UserDtos/ForgotPasswordDto.cs | 2 +- .../Models/UserDtos/ForgotPasswordPinDto.cs | 2 +- .../Devices/DeviceManager.cs | 5 + MediaBrowser.Controller/Dto/IDtoService.cs | 7 +- .../Library/IUserManager.cs | 10 +- .../Library/LibraryManagerExtensions.cs | 4 +- .../AuthHelper.cs | 28 ++++ .../Controllers/MusicGenreControllerTests.cs | 26 ++++ .../Controllers/UserControllerTests.cs | 12 +- .../Controllers/UserLibraryControllerTests.cs | 129 ++++++++++++++++++ .../Controllers/VideosControllerTests.cs | 27 ++++ 25 files changed, 370 insertions(+), 30 deletions(-) create mode 100644 tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs create mode 100644 tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs create mode 100644 tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs diff --git a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs index cdd7d8a52b..2ef244a0ad 100644 --- a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs +++ b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs @@ -2,6 +2,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.SyncPlay; @@ -47,6 +48,10 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy var userId = context.User.GetUserId(); var user = _userManager.GetUserById(userId); + if (user is null) + { + throw new ResourceNotFoundException(); + } if (requirement.RequiredAccess == SyncPlayAccessRequirementType.HasAccess) { diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index cc824c65ab..7261c47e95 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -99,12 +99,17 @@ public class ImageController : BaseJellyfinApiController [FromRoute, Required] ImageType imageType, [FromQuery] int? index = null) { + var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } + if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image."); } - var user = _userManager.GetUserById(userId); var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); await using (memoryStream.ConfigureAwait(false)) { @@ -148,12 +153,17 @@ public class ImageController : BaseJellyfinApiController [FromRoute, Required] ImageType imageType, [FromRoute] int index) { + var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } + if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image."); } - var user = _userManager.GetUserById(userId); var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); await using (memoryStream.ConfigureAwait(false)) { diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 134974dbe0..1bfc111af7 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -815,6 +815,11 @@ public class ItemsController : BaseJellyfinApiController [FromQuery] bool excludeActiveSessions = false) { var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } + var parentIdGuid = parentId ?? Guid.Empty; var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 830f84849e..a311554b4d 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -452,6 +452,10 @@ public class LibraryController : BaseJellyfinApiController if (user is not null) { parent = TranslateParentItem(parent, user); + if (parent is null) + { + break; + } } baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user)); diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 21b4243464..1aa1d8a100 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -1211,7 +1211,7 @@ public class LiveTvController : BaseJellyfinApiController private async Task AssertUserCanManageLiveTv() { - var user = _userManager.GetUserById(User.GetUserId()); + var user = _userManager.GetUserById(User.GetUserId()) ?? throw new ResourceNotFoundException(); var session = await _sessionManager.LogSessionActivity( User.GetClient(), User.GetVersion(), diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 302f138ebc..b08c8c4470 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -158,6 +158,11 @@ public class MusicGenresController : BaseJellyfinApiController item = _libraryManager.GetMusicGenre(genreName); } + if (item is null) + { + return NotFound(); + } + if (userId.HasValue && !userId.Value.Equals(default)) { var user = _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs index 18d6ebf1e0..ea8a59cfdf 100644 --- a/Jellyfin.Api/Controllers/PlaystateController.cs +++ b/Jellyfin.Api/Controllers/PlaystateController.cs @@ -77,6 +77,11 @@ public class PlaystateController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed) { var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } + var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var item = _libraryManager.GetItemById(itemId); @@ -89,6 +94,11 @@ public class PlaystateController : BaseJellyfinApiController foreach (var additionalUserInfo in session.AdditionalUsers) { var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId); + if (additionalUser is null) + { + return NotFound(); + } + UpdatePlayedStatus(additionalUser, item, true, datePlayed); } @@ -109,6 +119,11 @@ public class PlaystateController : BaseJellyfinApiController public async Task> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } + var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var item = _libraryManager.GetItemById(itemId); @@ -121,6 +136,11 @@ public class PlaystateController : BaseJellyfinApiController foreach (var additionalUserInfo in session.AdditionalUsers) { var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId); + if (additionalUser is null) + { + return NotFound(); + } + UpdatePlayedStatus(additionalUser, item, false, null); } diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index ef33644785..782fcadbb8 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -75,6 +75,10 @@ public class SessionController : BaseJellyfinApiController result = result.Where(i => i.SupportsRemoteControl); var user = _userManager.GetUserById(controllableByUserId.Value); + if (user is null) + { + return NotFound(); + } if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)) { diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 7f184f31e7..911e501321 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -147,6 +147,11 @@ public class UserController : BaseJellyfinApiController public async Task DeleteUser([FromRoute, Required] Guid userId) { var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } + await _sessionManager.RevokeUserTokens(user.Id, null).ConfigureAwait(false); await _userManager.DeleteUserAsync(userId).ConfigureAwait(false); return NoContent(); @@ -281,8 +286,8 @@ public class UserController : BaseJellyfinApiController { var success = await _userManager.AuthenticateUser( user.Username, - request.CurrentPw, - request.CurrentPw, + request.CurrentPw ?? string.Empty, + request.CurrentPw ?? string.Empty, HttpContext.GetNormalizedRemoteIp().ToString(), false).ConfigureAwait(false); @@ -292,7 +297,7 @@ public class UserController : BaseJellyfinApiController } } - await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false); + await _userManager.ChangePassword(user, request.NewPw ?? string.Empty).ConfigureAwait(false); var currentToken = User.GetToken(); @@ -338,7 +343,7 @@ public class UserController : BaseJellyfinApiController } else { - await _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword).ConfigureAwait(false); + await _userManager.ChangeEasyPassword(user, request.NewPw ?? string.Empty, request.NewPassword ?? string.Empty).ConfigureAwait(false); } return NoContent(); @@ -362,13 +367,17 @@ public class UserController : BaseJellyfinApiController [FromRoute, Required] Guid userId, [FromBody, Required] UserDto updateUser) { + var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } + if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) { return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed."); } - var user = _userManager.GetUserById(userId); - if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal)) { await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false); @@ -398,6 +407,10 @@ public class UserController : BaseJellyfinApiController [FromBody, Required] UserPolicy newPolicy) { var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } // If removing admin access if (!newPolicy.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator)) diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index 556cf38945..1e54a9781a 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -79,10 +79,18 @@ public class UserLibraryController : BaseJellyfinApiController public async Task> GetItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); + if (item is null) + { + return NotFound(); + } await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false); @@ -102,6 +110,11 @@ public class UserLibraryController : BaseJellyfinApiController public ActionResult GetRootFolder([FromRoute, Required] Guid userId) { var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } + var item = _libraryManager.GetUserRootFolder(); var dtoOptions = new DtoOptions().AddClientFields(User); return _dtoService.GetBaseItemDto(item, dtoOptions, user); @@ -119,10 +132,18 @@ public class UserLibraryController : BaseJellyfinApiController public async Task>> GetIntros([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); + if (item is null) + { + return NotFound(); + } var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false); var dtoOptions = new DtoOptions().AddClientFields(User); @@ -200,10 +221,18 @@ public class UserLibraryController : BaseJellyfinApiController public ActionResult> GetLocalTrailers([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); + if (item is null) + { + return NotFound(); + } var dtoOptions = new DtoOptions().AddClientFields(User); @@ -230,10 +259,18 @@ public class UserLibraryController : BaseJellyfinApiController public ActionResult> GetSpecialFeatures([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); + if (item is null) + { + return NotFound(); + } var dtoOptions = new DtoOptions().AddClientFields(User); @@ -275,6 +312,10 @@ public class UserLibraryController : BaseJellyfinApiController [FromQuery] bool groupItems = true) { var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } if (!isPlayed.HasValue) { diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 01a319879a..12aa47ead6 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -155,7 +155,12 @@ public class VideosController : BaseJellyfinApiController if (video.LinkedAlternateVersions.Length == 0) { - video = (Video)_libraryManager.GetItemById(video.PrimaryVersionId); + video = (Video?)_libraryManager.GetItemById(video.PrimaryVersionId); + } + + if (video is null) + { + return NotFound(); } foreach (var link in video.GetLinkedAlternateVersions()) diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index df37d96c60..5910d80737 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -200,7 +200,7 @@ public class MediaInfoHelper options.SubtitleStreamIndex = subtitleStreamIndex; } - var user = _userManager.GetUserById(userId); + var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException(); if (!enableDirectPlay) { diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 3ce2b834d1..0b7a4fa1ac 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -81,6 +81,11 @@ public static class RequestHelpers } var user = userManager.GetUserById(userId); + if (user is null) + { + throw new ResourceNotFoundException(); + } + return user.EnableUserPreferenceAccess; } @@ -98,7 +103,7 @@ public static class RequestHelpers if (session is null) { - throw new ArgumentException("Session not found."); + throw new ResourceNotFoundException("Session not found."); } return session; diff --git a/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs b/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs index 0503c5d57e..6b6d9682ba 100644 --- a/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs +++ b/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs @@ -1,4 +1,6 @@ -namespace Jellyfin.Api.Models.UserDtos; +using System.ComponentModel.DataAnnotations; + +namespace Jellyfin.Api.Models.UserDtos; /// /// The create user by name request body. @@ -8,7 +10,8 @@ public class CreateUserByName /// /// Gets or sets the username. /// - public string? Name { get; set; } + [Required] + required public string Name { get; set; } /// /// Gets or sets the password. diff --git a/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs b/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs index ebe9297ea6..a0631fd07b 100644 --- a/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs +++ b/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs @@ -11,5 +11,5 @@ public class ForgotPasswordDto /// Gets or sets the entered username to have its password reset. /// [Required] - public string? EnteredUsername { get; set; } + required public string EnteredUsername { get; set; } } diff --git a/Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs b/Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs index 2949efe29f..79b8a5d63f 100644 --- a/Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs +++ b/Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs @@ -11,5 +11,5 @@ public class ForgotPasswordPinDto /// Gets or sets the entered pin to have the password reset. /// [Required] - public string? Pin { get; set; } + required public string Pin { get; set; } } diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index 8b15d6823d..a4b4c19599 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -9,6 +9,7 @@ using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Data.Queries; using Jellyfin.Extensions; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Devices; @@ -185,6 +186,10 @@ namespace Jellyfin.Server.Implementations.Devices if (userId.HasValue) { var user = _userManager.GetUserById(userId.Value); + if (user is null) + { + throw new ResourceNotFoundException(); + } sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId)); } diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs index 89aafc84fb..22453f0f76 100644 --- a/MediaBrowser.Controller/Dto/IDtoService.cs +++ b/MediaBrowser.Controller/Dto/IDtoService.cs @@ -1,4 +1,3 @@ -#nullable disable #pragma warning disable CA1002 using System.Collections.Generic; @@ -28,7 +27,7 @@ namespace MediaBrowser.Controller.Dto /// The user. /// The owner. /// BaseItemDto. - BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null); + BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null); /// /// Gets the base item dtos. @@ -38,7 +37,7 @@ namespace MediaBrowser.Controller.Dto /// The user. /// The owner. /// The of . - IReadOnlyList GetBaseItemDtos(IReadOnlyList items, DtoOptions options, User user = null, BaseItem owner = null); + IReadOnlyList GetBaseItemDtos(IReadOnlyList items, DtoOptions options, User? user = null, BaseItem? owner = null); /// /// Gets the item by name dto. @@ -48,6 +47,6 @@ namespace MediaBrowser.Controller.Dto /// The list of tagged items. /// The user. /// The item dto. - BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List taggedItems, User user = null); + BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List? taggedItems, User? user = null); } } diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index 993e3e18f9..37b4afcf32 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -47,14 +45,14 @@ namespace MediaBrowser.Controller.Library /// The id. /// The user with the specified Id, or null if the user doesn't exist. /// id is an empty Guid. - User GetUserById(Guid id); + User? GetUserById(Guid id); /// /// Gets the name of the user by. /// /// The name. /// User. - User GetUserByName(string name); + User? GetUserByName(string name); /// /// Renames the user. @@ -128,7 +126,7 @@ namespace MediaBrowser.Controller.Library /// The user. /// The remote end point. /// UserDto. - UserDto GetUserDto(User user, string remoteEndPoint = null); + UserDto GetUserDto(User user, string? remoteEndPoint = null); /// /// Authenticates the user. @@ -139,7 +137,7 @@ namespace MediaBrowser.Controller.Library /// Remove endpoint to use. /// Specifies if a user session. /// User wrapped in awaitable task. - Task AuthenticateUser(string username, string password, string passwordSha1, string remoteEndPoint, bool isUserSession); + Task AuthenticateUser(string username, string password, string passwordSha1, string remoteEndPoint, bool isUserSession); /// /// Starts the forgot password process. diff --git a/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs index 7bc8fa5abd..6d2c3c3d29 100644 --- a/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs +++ b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -9,7 +7,7 @@ namespace MediaBrowser.Controller.Library { public static class LibraryManagerExtensions { - public static BaseItem GetItemById(this ILibraryManager manager, string id) + public static BaseItem? GetItemById(this ILibraryManager manager, string id) { return manager.GetItemById(new Guid(id)); } diff --git a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs index 9eb0beda44..3737fee0ac 100644 --- a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs +++ b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Models.StartupDtos; using Jellyfin.Api.Models.UserDtos; using Jellyfin.Extensions.Json; +using MediaBrowser.Model.Dto; using Xunit; namespace Jellyfin.Server.Integration.Tests @@ -43,6 +44,33 @@ namespace Jellyfin.Server.Integration.Tests return auth!.AccessToken; } + public static async Task GetUserDtoAsync(HttpClient client) + { + using var response = await client.GetAsync("Users/Me").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var userDto = await JsonSerializer.DeserializeAsync( + await response.Content.ReadAsStreamAsync().ConfigureAwait(false), JsonDefaults.Options).ConfigureAwait(false); + Assert.NotNull(userDto); + return userDto; + } + + public static async Task GetRootFolderDtoAsync(HttpClient client, Guid userId = default) + { + if (userId.Equals(default)) + { + var userDto = await GetUserDtoAsync(client).ConfigureAwait(false); + userId = userDto.Id; + } + + var response = await client.GetAsync($"Users/{userId}/Items/Root").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var rootDto = await JsonSerializer.DeserializeAsync( + await response.Content.ReadAsStreamAsync().ConfigureAwait(false), + JsonDefaults.Options).ConfigureAwait(false); + Assert.NotNull(rootDto); + return rootDto; + } + public static void AddAuthHeader(this HttpHeaders headers, string accessToken) { headers.Add(AuthHeaderName, DummyAuthHeader + $", Token={accessToken}"); diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs new file mode 100644 index 0000000000..17f3dc99fd --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs @@ -0,0 +1,26 @@ +using System.Net; +using System.Threading.Tasks; +using Xunit; + +namespace Jellyfin.Server.Integration.Tests.Controllers; + +public sealed class MusicGenreControllerTests : IClassFixture +{ + private readonly JellyfinApplicationFactory _factory; + private static string? _accessToken; + + public MusicGenreControllerTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task MusicGenres_FakeMusicGenre_NotFound() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var response = await client.GetAsync("MusicGenres/Fake-MusicGenre").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs index 2b825a93a0..e5cde66762 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs @@ -66,6 +66,16 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.False(users![0].HasConfiguredPassword); } + [Fact] + [Priority(-1)] + public async Task Me_Valid_Success() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + _ = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + } + [Fact] [Priority(0)] public async Task New_Valid_Success() @@ -108,7 +118,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers var createRequest = new CreateUserByName() { - Name = username + Name = username! }; using var response = await CreateUserByName(client, createRequest).ConfigureAwait(false); diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs new file mode 100644 index 0000000000..69f2ccf339 --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs @@ -0,0 +1,129 @@ +using System; +using System.Globalization; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Jellyfin.Extensions.Json; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Querying; +using Xunit; + +namespace Jellyfin.Server.Integration.Tests.Controllers; + +public sealed class UserLibraryControllerTests : IClassFixture +{ + private readonly JellyfinApplicationFactory _factory; + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; + private static string? _accessToken; + + public UserLibraryControllerTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task GetRootFolder_NonExistenUserId_NotFound() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var response = await client.GetAsync($"Users/{Guid.NewGuid()}/Items/Root").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task GetRootFolder_UserId_Valid() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + _ = await AuthHelper.GetRootFolderDtoAsync(client).ConfigureAwait(false); + } + + [Theory] + [InlineData("Users/{0}/Items/{1}")] + [InlineData("Users/{0}/Items/{1}/Intros")] + [InlineData("Users/{0}/Items/{1}/LocalTrailers")] + [InlineData("Users/{0}/Items/{1}/SpecialFeatures")] + [InlineData("Users/{0}/Items/{1}/Lyrics")] + public async Task GetItem_NonExistenUserId_NotFound(string format) + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client).ConfigureAwait(false); + + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid(), rootFolderDto.Id)).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData("Users/{0}/Items/{1}")] + [InlineData("Users/{0}/Items/{1}/Intros")] + [InlineData("Users/{0}/Items/{1}/LocalTrailers")] + [InlineData("Users/{0}/Items/{1}/SpecialFeatures")] + [InlineData("Users/{0}/Items/{1}/Lyrics")] + public async Task GetItem_NonExistentItemId_NotFound(string format) + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, Guid.NewGuid())).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task GetItem_UserIdAndItemId_Valid() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false); + + var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var rootDto = await JsonSerializer.DeserializeAsync( + await response.Content.ReadAsStreamAsync().ConfigureAwait(false), + _jsonOptions).ConfigureAwait(false); + Assert.NotNull(rootDto); + } + + [Fact] + public async Task GetIntros_UserIdAndItemId_Valid() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false); + + var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}/Intros").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var rootDto = await JsonSerializer.DeserializeAsync>( + await response.Content.ReadAsStreamAsync().ConfigureAwait(false), + _jsonOptions).ConfigureAwait(false); + Assert.NotNull(rootDto); + } + + [Theory] + [InlineData("Users/{0}/Items/{1}/LocalTrailers")] + [InlineData("Users/{0}/Items/{1}/SpecialFeatures")] + public async Task LocalTrailersAndSpecialFeatures_UserIdAndItemId_Valid(string format) + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false); + + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, rootFolderDto.Id)).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var rootDto = await JsonSerializer.DeserializeAsync( + await response.Content.ReadAsStreamAsync().ConfigureAwait(false), + _jsonOptions).ConfigureAwait(false); + Assert.NotNull(rootDto); + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs new file mode 100644 index 0000000000..0f9a2e90aa --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs @@ -0,0 +1,27 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Xunit; + +namespace Jellyfin.Server.Integration.Tests.Controllers; + +public sealed class VideosControllerTests : IClassFixture +{ + private readonly JellyfinApplicationFactory _factory; + private static string? _accessToken; + + public VideosControllerTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task DeleteAlternateSources_NonExistentItemId_NotFound() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var response = await client.DeleteAsync($"Videos/{Guid.NewGuid()}").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } +} From eb7fee95906f1c9d8d104777e0214de9115ca82f Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sat, 4 Feb 2023 21:08:21 +0100 Subject: [PATCH 05/69] Add more tests --- Jellyfin.Api/Controllers/ItemsController.cs | 3 +- Jellyfin.Api/Controllers/LibraryController.cs | 10 +++ .../Controllers/ItemsControllerTests.cs | 64 +++++++++++++++++++ .../Controllers/LibraryControllerTests.cs | 40 ++++++++++++ .../Controllers/PlaystateControllerTests.cs | 45 ++++++++----- .../Controllers/SessionControllerTests.cs | 27 ++++++++ .../Controllers/UserControllerTests.cs | 13 ++++ 7 files changed, 184 insertions(+), 18 deletions(-) create mode 100644 tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs create mode 100644 tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs create mode 100644 tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 1bfc111af7..c937176cdf 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -6,6 +6,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -241,7 +242,7 @@ public class ItemsController : BaseJellyfinApiController var isApiKey = User.GetIsApiKey(); // if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method var user = !isApiKey && userId.HasValue && !userId.Value.Equals(default) - ? _userManager.GetUserById(userId.Value) + ? _userManager.GetUserById(userId.Value) ?? throw new ResourceNotFoundException() : null; // beyond this point, we're either using an api key or we have a valid user diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index a311554b4d..c4309412c3 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -283,6 +283,11 @@ public class LibraryController : BaseJellyfinApiController userId, inheritFromParent); + if (themeSongs.Result is NotFoundObjectResult || themeVideos.Result is NotFoundObjectResult) + { + return NotFound(); + } + return new AllThemeMediaResult { ThemeSongsResult = themeSongs?.Value, @@ -676,6 +681,11 @@ public class LibraryController : BaseJellyfinApiController : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); + if (item is null) + { + return NotFound(); + } + if (item is Episode || (item is IItemByName && item is not MusicArtist)) { return new QueryResult(); diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs new file mode 100644 index 0000000000..62b32b92e7 --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs @@ -0,0 +1,64 @@ +using System; +using System.Globalization; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Jellyfin.Extensions.Json; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Querying; +using Xunit; + +namespace Jellyfin.Server.Integration.Tests.Controllers; + +public sealed class ItemsControllerTests : IClassFixture +{ + private readonly JellyfinApplicationFactory _factory; + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; + private static string? _accessToken; + + public ItemsControllerTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task GetItems_NoApiKeyOrUserId_BadRequest() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var response = await client.GetAsync("Items").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Theory] + [InlineData("Users/{0}/Items")] + [InlineData("Users/{0}/Items/Resume")] + public async Task GetUserItems_NonExistentUserId_NotFound(string format) + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData("Items?userId={0}")] + [InlineData("Users/{0}/Items")] + [InlineData("Users/{0}/Items/Resume")] + public async Task GetItems_UserId_Ok(string format) + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id)).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var items = await JsonSerializer.DeserializeAsync>( + await response.Content.ReadAsStreamAsync().ConfigureAwait(false), + _jsonOptions).ConfigureAwait(false); + Assert.NotNull(items); + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs new file mode 100644 index 0000000000..013d19a9fd --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs @@ -0,0 +1,40 @@ +using System; +using System.Globalization; +using System.Net; +using System.Threading.Tasks; +using Xunit; + +namespace Jellyfin.Server.Integration.Tests.Controllers; + +public sealed class LibraryControllerTests : IClassFixture +{ + private readonly JellyfinApplicationFactory _factory; + private static string? _accessToken; + + public LibraryControllerTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Theory] + [InlineData("Items/{0}/File")] + [InlineData("Items/{0}/ThemeSongs")] + [InlineData("Items/{0}/ThemeVideos")] + [InlineData("Items/{0}/ThemeMedia")] + [InlineData("Items/{0}/Ancestors")] + [InlineData("Items/{0}/Download")] + [InlineData("Artists/{0}/Similar")] + [InlineData("Items/{0}/Similar")] + [InlineData("Albums/{0}/Similar")] + [InlineData("Shows/{0}/Similar")] + [InlineData("Movies/{0}/Similar")] + [InlineData("Trailers/{0}/Similar")] + public async Task Get_NonExistentItemId_NotFound(string format) + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs index f8f5fecec4..868ecd53f5 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs @@ -1,18 +1,13 @@ using System; using System.Net; -using System.Net.Http; using System.Threading.Tasks; using Xunit; -using Xunit.Priority; namespace Jellyfin.Server.Integration.Tests.Controllers; -[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)] public class PlaystateControllerTests : IClassFixture { private readonly JellyfinApplicationFactory _factory; - private static readonly Guid _testUserId = Guid.NewGuid(); - private static readonly Guid _testItemId = Guid.NewGuid(); private static string? _accessToken; public PlaystateControllerTests(JellyfinApplicationFactory factory) @@ -20,31 +15,47 @@ public class PlaystateControllerTests : IClassFixture DeleteUserPlayedItems(HttpClient httpClient, Guid userId, Guid itemId) - => httpClient.DeleteAsync($"Users/{userId}/PlayedItems/{itemId}"); - - private Task PostUserPlayedItems(HttpClient httpClient, Guid userId, Guid itemId) - => httpClient.PostAsync($"Users/{userId}/PlayedItems/{itemId}", null); - [Fact] - [Priority(0)] - public async Task DeleteMarkUnplayedItem_DoesNotExist_NotFound() + public async Task DeleteMarkUnplayedItem_NonExistentUserId_NotFound() { var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); - using var response = await DeleteUserPlayedItems(client, _testUserId, _testItemId).ConfigureAwait(false); + using var response = await client.DeleteAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}").ConfigureAwait(false); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] - [Priority(0)] - public async Task PostMarkPlayedItem_DoesNotExist_NotFound() + public async Task PostMarkPlayedItem_NonExistentUserId_NotFound() { var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); - using var response = await PostUserPlayedItems(client, _testUserId, _testItemId).ConfigureAwait(false); + using var response = await client.PostAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}", null).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task DeleteMarkUnplayedItem_NonExistentItemId_NotFound() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + + using var response = await client.DeleteAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task PostMarkPlayedItem_NonExistentItemId_NotFound() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + + using var response = await client.PostAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}", null).ConfigureAwait(false); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs new file mode 100644 index 0000000000..cb0a829e8e --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs @@ -0,0 +1,27 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Xunit; + +namespace Jellyfin.Server.Integration.Tests.Controllers; + +public class SessionControllerTests : IClassFixture +{ + private readonly JellyfinApplicationFactory _factory; + private static string? _accessToken; + + public SessionControllerTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task GetSessions_NonExistentUserId_NotFound() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + using var response = await client.GetAsync($"Session/Sessions?userId={Guid.NewGuid()}").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs index e5cde66762..2a3c53dbe4 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs @@ -125,6 +125,19 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } + [Fact] + [Priority(0)] + public async Task Delete_DoesntExist_NotFound() + { + var client = _factory.CreateClient(); + + // access token can't be null here as the previous test populated it + client.DefaultRequestHeaders.AddAuthHeader(_accessToken!); + + using var response = await client.DeleteAsync($"User/{Guid.NewGuid()}").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + [Fact] [Priority(1)] public async Task UpdateUserPassword_Valid_Success() From b6b6b53f7c1003c29eeca5dbf15e69219f5a474c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Feb 2023 01:00:02 +0000 Subject: [PATCH 06/69] chore(deps): update github/codeql-action digest to 39d8d7e --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5779ac3cf9..9004cddd82 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2 + uses: github/codeql-action/init@39d8d7e78f59cf6b40ac3b9fbebef0c753d7c9e5 # v2 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2 + uses: github/codeql-action/autobuild@39d8d7e78f59cf6b40ac3b9fbebef0c753d7c9e5 # v2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2 + uses: github/codeql-action/analyze@39d8d7e78f59cf6b40ac3b9fbebef0c753d7c9e5 # v2 From 15b6d1672db17fc467301e1d6f564f1844932ba4 Mon Sep 17 00:00:00 2001 From: Jpuc1143 <80900349+Jpuc1143@users.noreply.github.com> Date: Tue, 7 Feb 2023 22:29:33 -0300 Subject: [PATCH 07/69] Removed unnecesary migration code Co-authored-by: David Ullmer --- Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index 1efd071ec5..ea2f033027 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -170,7 +170,6 @@ namespace Jellyfin.Server.Migrations.Routines } user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags); - user.SetPreference(PreferenceKind.AllowedTags, policy.AllowedTags); user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels); user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices); user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders); From c70516bcd26501c881456487d3460488a3955764 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Feb 2023 13:58:33 +0000 Subject: [PATCH 08/69] chore(deps): update peter-evans/create-or-update-comment digest to 67dcc54 --- .github/workflows/commands.yml | 10 +++++----- .github/workflows/openapi.yml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 5d945c001b..75227c57b9 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify as seen - uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2 with: token: ${{ secrets.JF_BOT_TOKEN }} comment-id: ${{ github.event.comment.id }} @@ -43,7 +43,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify as seen - uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2 if: ${{ github.event.comment != null }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -58,7 +58,7 @@ jobs: - name: Notify as running id: comment_running - uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2 if: ${{ github.event.comment != null }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -93,7 +93,7 @@ jobs: exit ${retcode} - name: Notify with result success - uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2 if: ${{ github.event.comment != null && success() }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -108,7 +108,7 @@ jobs: reactions: hooray - name: Notify with result failure - uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2 if: ${{ github.event.comment != null && failure() }} with: token: ${{ secrets.JF_BOT_TOKEN }} diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index 4577ff5251..e2b98efd0b 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -110,7 +110,7 @@ jobs: direction: last body-includes: openapi-diff-workflow-comment - name: Reply or edit difference comment (changed) - uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2 if: ${{ steps.read-diff.outputs.body != '' }} with: issue-number: ${{ github.event.pull_request.number }} @@ -125,7 +125,7 @@ jobs: - name: Edit difference comment (unchanged) - uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2 if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }} with: issue-number: ${{ github.event.pull_request.number }} From a4edcbf2035c616627d5afde5d49fe01e057b7a9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Feb 2023 13:58:41 +0000 Subject: [PATCH 09/69] chore(deps): update peter-evans/find-comment digest to 85a676a --- .github/workflows/openapi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index 4577ff5251..7f81e1595c 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -103,7 +103,7 @@ jobs: body="${body//$'\r'/'%0D'}" echo ::set-output name=body::$body - name: Find difference comment - uses: peter-evans/find-comment@81e2da3af01c92f83cb927cf3ace0e085617c556 # v2 + uses: peter-evans/find-comment@85a676a52594b4481e0532825a2d8906ef96dac2 # v2 id: find-comment with: issue-number: ${{ github.event.pull_request.number }} From ef4ae9a2dd9d5aff87262910e87d734ef3183125 Mon Sep 17 00:00:00 2001 From: gnattu Date: Thu, 9 Feb 2023 06:42:17 +0800 Subject: [PATCH 10/69] Implement hardware filters for videotoolbox, use Apple AAC encoder when available (#7807) --- .../MediaEncoding/EncodingHelper.cs | 86 +++++++++++++++++++ .../Encoder/EncoderValidator.cs | 6 +- 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 9b5edabc06..14547d4406 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -559,6 +559,12 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase)) { + // Use Apple's aac encoder if available as it provides best audio quality + if (_mediaEncoder.SupportsEncoder("aac_at")) + { + return "aac_at"; + } + // Use libfdk_aac for better audio quality if using custom build of FFmpeg which has fdk_aac support if (_mediaEncoder.SupportsEncoder("libfdk_aac")) { @@ -2814,6 +2820,13 @@ namespace MediaBrowser.Controller.MediaEncoding { return "deinterlace_qsv=mode=2"; } + else if (hwDeintSuffix.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase)) + { + return string.Format( + CultureInfo.InvariantCulture, + "yadif_videotoolbox={0}:-1:0", + doubleRateDeint ? "1" : "0"); + } return string.Empty; } @@ -4450,6 +4463,75 @@ namespace MediaBrowser.Controller.MediaEncoding return (mainFilters, subFilters, overlayFilters); } + /// + /// Gets the parameter of Apple VideoToolBox filter chain. + /// + /// Encoding state. + /// Encoding options. + /// Video encoder to use. + /// The tuple contains three lists: main, sub and overlay filters. + public (List MainFilters, List SubFilters, List OverlayFilters) GetAppleVidFilterChain( + EncodingJobInfo state, + EncodingOptions options, + string vidEncoder) + { + if (!string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) + { + return (null, null, null); + } + + var swFilterChain = GetSwVidFilterChain(state, options, vidEncoder); + + if (!options.EnableHardwareEncoding) + { + return swFilterChain; + } + + if (_mediaEncoder.EncoderVersion.CompareTo(new Version("5.0.0")) < 0) + { + // All features used here requires ffmpeg 5.0 or later, fallback to software filters if using an old ffmpeg + return swFilterChain; + } + + var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); + var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); + var doDeintH2645 = doDeintH264 || doDeintHevc; + var inW = state.VideoStream?.Width; + var inH = state.VideoStream?.Height; + var reqW = state.BaseRequest.Width; + var reqH = state.BaseRequest.Height; + var reqMaxW = state.BaseRequest.MaxWidth; + var reqMaxH = state.BaseRequest.MaxHeight; + var threeDFormat = state.MediaSource.Video3DFormat; + var newfilters = new List(); + var noOverlay = swFilterChain.OverlayFilters.Count == 0; + var supportsHwDeint = _mediaEncoder.SupportsFilter("yadif_videotoolbox"); + // fallback to software filters if we are using filters not supported by hardware yet. + var useHardwareFilters = noOverlay && (!doDeintH2645 || supportsHwDeint); + + if (!useHardwareFilters) + { + return swFilterChain; + } + + // ffmpeg cannot use videotoolbox to scale + var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + newfilters.Add(swScaleFilter); + + // hwupload on videotoolbox encoders can automatically convert AVFrame into its CVPixelBuffer equivalent + // videotoolbox will automatically convert the CVPixelBuffer to a pixel format the encoder supports, so we don't have to set a pixel format explicitly here + // This will reduce CPU usage significantly on UHD videos with 10 bit colors because we bypassed the ffmpeg pixel format conversion + newfilters.Add("hwupload"); + + if (doDeintH2645) + { + var deintFilter = GetHwDeinterlaceFilter(state, options, "videotoolbox"); + newfilters.Add(deintFilter); + } + + return (newfilters, swFilterChain.SubFilters, swFilterChain.OverlayFilters); + } + /// /// Gets the parameter of video processing filters. /// @@ -4492,6 +4574,10 @@ namespace MediaBrowser.Controller.MediaEncoding { (mainFilters, subFilters, overlayFilters) = GetAmdVidFilterChain(state, options, outputVideoCodec); } + else if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) + { + (mainFilters, subFilters, overlayFilters) = GetAppleVidFilterChain(state, options, outputVideoCodec); + } else { (mainFilters, subFilters, overlayFilters) = GetSwVidFilterChain(state, options, outputVideoCodec); diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 8479b7d50d..9e6134b524 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -56,6 +56,7 @@ namespace MediaBrowser.MediaEncoding.Encoder "libvpx", "libvpx-vp9", "aac", + "aac_at", "libfdk_aac", "ac3", "libmp3lame", @@ -106,7 +107,10 @@ namespace MediaBrowser.MediaEncoding.Encoder // vulkan "libplacebo", "scale_vulkan", - "overlay_vulkan" + "overlay_vulkan", + "hwupload_vaapi", + // videotoolbox + "yadif_videotoolbox" }; private static readonly IReadOnlyDictionary _filterOptionsDict = new Dictionary From 26c0d18405a609be121018ea323e72a9f407b9d0 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Thu, 9 Feb 2023 01:27:55 +0100 Subject: [PATCH 11/69] Remove .npmrc and nuget.config files --- .npmrc | 3 --- nuget.config | 6 ------ 2 files changed, 9 deletions(-) delete mode 100644 .npmrc delete mode 100644 nuget.config diff --git a/.npmrc b/.npmrc deleted file mode 100644 index b7a317000b..0000000000 --- a/.npmrc +++ /dev/null @@ -1,3 +0,0 @@ -registry=https://registry.npmjs.org/ -@jellyfin:registry=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/ -always-auth=true \ No newline at end of file diff --git a/nuget.config b/nuget.config deleted file mode 100644 index 326331f322..0000000000 --- a/nuget.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - From fd0b6192199746f135de41957a42970fbc872a34 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Feb 2023 19:10:48 -0700 Subject: [PATCH 12/69] chore(deps): update github/codeql-action digest to 8775e86 (#9280) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9004cddd82..ef9ab45a17 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@39d8d7e78f59cf6b40ac3b9fbebef0c753d7c9e5 # v2 + uses: github/codeql-action/init@8775e868027fa230df8586bdf502bbd9b618a477 # v2 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@39d8d7e78f59cf6b40ac3b9fbebef0c753d7c9e5 # v2 + uses: github/codeql-action/autobuild@8775e868027fa230df8586bdf502bbd9b618a477 # v2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@39d8d7e78f59cf6b40ac3b9fbebef0c753d7c9e5 # v2 + uses: github/codeql-action/analyze@8775e868027fa230df8586bdf502bbd9b618a477 # v2 From 209edd38a4163a8cf4abd5e47bfe0ea1a100f351 Mon Sep 17 00:00:00 2001 From: cvium Date: Wed, 8 Feb 2023 23:55:26 +0100 Subject: [PATCH 13/69] refactor: simplify authz --- .../AnonymousLanAccessHandler.cs | 3 +- Jellyfin.Api/Auth/BaseAuthorizationHandler.cs | 113 -------------- .../DefaultAuthorizationHandler.cs | 47 +++++- .../DefaultAuthorizationRequirement.cs | 13 ++ .../Auth/DownloadPolicy/DownloadHandler.cs | 44 ------ .../DownloadPolicy/DownloadRequirement.cs | 11 -- ...TimeOrIgnoreParentalControlSetupHandler.cs | 56 ------- ...OrIgnoreParentalControlSetupRequirement.cs | 11 -- .../FirstTimeSetupOrDefaultRequirement.cs | 11 -- .../FirstTimeSetupOrElevatedHandler.cs | 57 ------- .../FirstTimeSetupOrElevatedRequirement.cs | 11 -- .../FirstTimeSetupHandler.cs} | 43 +++--- .../FirstTimeSetupRequirement.cs | 26 ++++ .../IgnoreParentalControlHandler.cs | 44 ------ .../IgnoreParentalControlRequirement.cs | 11 -- .../LocalAccessOrRequiresElevationHandler.cs | 45 ------ ...calAccessOrRequiresElevationRequirement.cs | 11 -- .../LocalAccessPolicy/LocalAccessHandler.cs | 44 ------ .../LocalAccessRequirement.cs | 11 -- .../RequiresElevationHandler.cs | 45 ------ .../RequiresElevationRequirement.cs | 11 -- .../SyncPlayAccessHandler.cs | 41 +---- .../SyncPlayAccessRequirement.cs | 6 +- .../UserPermissionHandler.cs | 37 +++++ .../UserPermissionRequirement.cs | 26 ++++ Jellyfin.Api/Constants/Policies.cs | 5 - Jellyfin.Api/Controllers/ArtistsController.cs | 3 +- .../Controllers/ChannelsController.cs | 3 +- .../Controllers/ClientLogController.cs | 3 +- .../Controllers/CollectionController.cs | 3 +- .../Controllers/ConfigurationController.cs | 2 +- .../Controllers/DashboardController.cs | 3 +- .../DisplayPreferencesController.cs | 3 +- .../Controllers/DynamicHlsController.cs | 3 +- Jellyfin.Api/Controllers/FilterController.cs | 3 +- Jellyfin.Api/Controllers/GenresController.cs | 5 +- .../Controllers/HlsSegmentController.cs | 5 +- Jellyfin.Api/Controllers/ImageController.cs | 18 +-- .../Controllers/InstantMixController.cs | 3 +- .../Controllers/ItemLookupController.cs | 2 +- Jellyfin.Api/Controllers/ItemsController.cs | 3 +- Jellyfin.Api/Controllers/LibraryController.cs | 80 +++++----- Jellyfin.Api/Controllers/LiveTvController.cs | 77 +++++----- .../Controllers/MediaInfoController.cs | 3 +- Jellyfin.Api/Controllers/MoviesController.cs | 3 +- .../Controllers/MusicGenresController.cs | 3 +- Jellyfin.Api/Controllers/PackageController.cs | 2 +- Jellyfin.Api/Controllers/PersonsController.cs | 3 +- .../Controllers/PlaylistsController.cs | 3 +- .../Controllers/PlaystateController.cs | 3 +- Jellyfin.Api/Controllers/PluginsController.cs | 2 +- .../Controllers/QuickConnectController.cs | 2 +- .../Controllers/RemoteImageController.cs | 4 +- Jellyfin.Api/Controllers/SearchController.cs | 3 +- Jellyfin.Api/Controllers/SessionController.cs | 28 ++-- Jellyfin.Api/Controllers/StudiosController.cs | 3 +- .../Controllers/SubtitleController.cs | 12 +- .../Controllers/SuggestionsController.cs | 3 +- Jellyfin.Api/Controllers/SystemController.cs | 4 +- .../Controllers/TrailersController.cs | 3 +- Jellyfin.Api/Controllers/TvShowsController.cs | 3 +- .../Controllers/UniversalAudioController.cs | 3 +- Jellyfin.Api/Controllers/UserController.cs | 12 +- .../Controllers/UserLibraryController.cs | 3 +- .../Controllers/UserViewsController.cs | 3 +- Jellyfin.Api/Controllers/VideosController.cs | 2 +- Jellyfin.Api/Controllers/YearsController.cs | 3 +- .../Middleware/LanFilteringMiddleware.cs | 11 +- Jellyfin.Data/DayOfWeekHelper.cs | 11 ++ Jellyfin.Data/Entities/User.cs | 3 +- .../ApiServiceCollectionExtensions.cs | 143 ++++-------------- .../FirstTimeSetupHandlerTests.cs} | 19 +-- .../IgnoreScheduleHandlerTests.cs | 8 +- .../LocalAccessHandlerTests.cs | 59 -------- .../RequiresElevationHandlerTests.cs | 53 ------- 75 files changed, 395 insertions(+), 1027 deletions(-) delete mode 100644 Jellyfin.Api/Auth/BaseAuthorizationHandler.cs delete mode 100644 Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs delete mode 100644 Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs delete mode 100644 Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs delete mode 100644 Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs delete mode 100644 Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs delete mode 100644 Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs delete mode 100644 Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedRequirement.cs rename Jellyfin.Api/Auth/{FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs => FirstTimeSetupPolicy/FirstTimeSetupHandler.cs} (58%) create mode 100644 Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupRequirement.cs delete mode 100644 Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs delete mode 100644 Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs delete mode 100644 Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs delete mode 100644 Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs delete mode 100644 Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs delete mode 100644 Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs delete mode 100644 Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs delete mode 100644 Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationRequirement.cs create mode 100644 Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs create mode 100644 Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs rename tests/Jellyfin.Api.Tests/Auth/{FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs => FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs} (80%) delete mode 100644 tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs delete mode 100644 tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs diff --git a/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs b/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs index d4b1ffb060..741b88ea95 100644 --- a/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs +++ b/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -29,7 +30,7 @@ namespace Jellyfin.Api.Auth.AnonymousLanAccessPolicy /// protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AnonymousLanAccessRequirement requirement) { - var ip = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress; + var ip = _httpContextAccessor.HttpContext?.GetNormalizedRemoteIp(); // Loopback will be on LAN, so we can accept null. if (ip is null || _networkManager.IsInLocalNetwork(ip)) diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs deleted file mode 100644 index 8e5e66d64a..0000000000 --- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Security.Claims; -using Jellyfin.Api.Extensions; -using Jellyfin.Api.Helpers; -using Jellyfin.Data.Enums; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Library; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; - -namespace Jellyfin.Api.Auth -{ - /// - /// Base authorization handler. - /// - /// Type of Authorization Requirement. - public abstract class BaseAuthorizationHandler : AuthorizationHandler - where T : IAuthorizationRequirement - { - private readonly IUserManager _userManager; - private readonly INetworkManager _networkManager; - private readonly IHttpContextAccessor _httpContextAccessor; - - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - protected BaseAuthorizationHandler( - IUserManager userManager, - INetworkManager networkManager, - IHttpContextAccessor httpContextAccessor) - { - _userManager = userManager; - _networkManager = networkManager; - _httpContextAccessor = httpContextAccessor; - } - - /// - /// Validate authenticated claims. - /// - /// Request claims. - /// Whether to ignore parental control. - /// Whether access is to be allowed locally only. - /// Whether validation requires download permission. - /// Validated claim status. - protected bool ValidateClaims( - ClaimsPrincipal claimsPrincipal, - bool ignoreSchedule = false, - bool localAccessOnly = false, - bool requiredDownloadPermission = false) - { - // ApiKey is currently global admin, always allow. - var isApiKey = claimsPrincipal.GetIsApiKey(); - if (isApiKey) - { - return true; - } - - // Ensure claim has userId. - var userId = claimsPrincipal.GetUserId(); - if (userId.Equals(default)) - { - return false; - } - - // Ensure userId links to a valid user. - var user = _userManager.GetUserById(userId); - if (user is null) - { - return false; - } - - // Ensure user is not disabled. - if (user.HasPermission(PermissionKind.IsDisabled)) - { - return false; - } - - var isInLocalNetwork = _httpContextAccessor.HttpContext is not null - && _networkManager.IsInLocalNetwork(_httpContextAccessor.HttpContext.GetNormalizedRemoteIp()); - - // User cannot access remotely and user is remote - if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !isInLocalNetwork) - { - return false; - } - - if (localAccessOnly && !isInLocalNetwork) - { - return false; - } - - // User attempting to access out of parental control hours. - if (!ignoreSchedule - && !user.HasPermission(PermissionKind.IsAdministrator) - && !user.IsParentalScheduleAllowed()) - { - return false; - } - - // User attempting to download without permission. - if (requiredDownloadPermission - && !user.HasPermission(PermissionKind.EnableContentDownloading)) - { - return false; - } - - return true; - } - } -} diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs index be77b7a4e4..7489e2a35c 100644 --- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs @@ -1,4 +1,8 @@ using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; +using Jellyfin.Data.Enums; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; @@ -9,8 +13,12 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy /// /// Default authorization handler. /// - public class DefaultAuthorizationHandler : BaseAuthorizationHandler + public class DefaultAuthorizationHandler : AuthorizationHandler { + private readonly IUserManager _userManager; + private readonly INetworkManager _networkManager; + private readonly IHttpContextAccessor _httpContextAccessor; + /// /// Initializes a new instance of the class. /// @@ -21,21 +29,50 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy IUserManager userManager, INetworkManager networkManager, IHttpContextAccessor httpContextAccessor) - : base(userManager, networkManager, httpContextAccessor) { + _userManager = userManager; + _networkManager = networkManager; + _httpContextAccessor = httpContextAccessor; } /// protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement) { - var validated = ValidateClaims(context.User); - if (validated) + // Admins can do everything + if (context.User.GetIsApiKey() || context.User.IsInRole(UserRoles.Administrator)) { context.Succeed(requirement); + return Task.CompletedTask; } - else + + var userId = context.User.GetUserId(); + // This likely only happens during the wizard, so skip the default checks and let any other handlers do it + if (userId.Equals(default)) + { + return Task.CompletedTask; + } + + var isInLocalNetwork = _httpContextAccessor.HttpContext is not null + && _networkManager.IsInLocalNetwork(_httpContextAccessor.HttpContext.GetNormalizedRemoteIp()); + var user = _userManager.GetUserById(userId); + // User cannot access remotely and user is remote + if (!isInLocalNetwork && !user.HasPermission(PermissionKind.EnableRemoteAccess)) { context.Fail(); + return Task.CompletedTask; + } + + // It's not great to have this check, but parental schedule must usually be honored except in a few rare cases + if (requirement.ValidateParentalSchedule && !user.IsParentalScheduleAllowed()) + { + context.Fail(); + return Task.CompletedTask; + } + + // Only succeed if the requirement isn't a subclass as any subclassed requirement will handle success in its own handler + if (requirement.GetType() == typeof(DefaultAuthorizationRequirement)) + { + context.Succeed(requirement); } return Task.CompletedTask; diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs index 7cea00b694..0846e7515a 100644 --- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs @@ -7,5 +7,18 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy /// public class DefaultAuthorizationRequirement : IAuthorizationRequirement { + /// + /// Initializes a new instance of the class. + /// + /// A value indicating whether to validate parental schedule. + public DefaultAuthorizationRequirement(bool validateParentalSchedule = true) + { + ValidateParentalSchedule = validateParentalSchedule; + } + + /// + /// Gets a value indicating whether to ignore parental schedule. + /// + public bool ValidateParentalSchedule { get; init; } } } diff --git a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs deleted file mode 100644 index b61680ab1a..0000000000 --- a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Library; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; - -namespace Jellyfin.Api.Auth.DownloadPolicy -{ - /// - /// Download authorization handler. - /// - public class DownloadHandler : BaseAuthorizationHandler - { - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public DownloadHandler( - IUserManager userManager, - INetworkManager networkManager, - IHttpContextAccessor httpContextAccessor) - : base(userManager, networkManager, httpContextAccessor) - { - } - - /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DownloadRequirement requirement) - { - var validated = ValidateClaims(context.User); - if (validated) - { - context.Succeed(requirement); - } - else - { - context.Fail(); - } - - return Task.CompletedTask; - } - } -} diff --git a/Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs b/Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs deleted file mode 100644 index b0a72a9dec..0000000000 --- a/Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace Jellyfin.Api.Auth.DownloadPolicy -{ - /// - /// The download permission requirement. - /// - public class DownloadRequirement : IAuthorizationRequirement - { - } -} diff --git a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs deleted file mode 100644 index 31482a930f..0000000000 --- a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Library; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; - -namespace Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy -{ - /// - /// Ignore parental control schedule and allow before startup wizard has been completed. - /// - public class FirstTimeOrIgnoreParentalControlSetupHandler : BaseAuthorizationHandler - { - private readonly IConfigurationManager _configurationManager; - - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public FirstTimeOrIgnoreParentalControlSetupHandler( - IUserManager userManager, - INetworkManager networkManager, - IHttpContextAccessor httpContextAccessor, - IConfigurationManager configurationManager) - : base(userManager, networkManager, httpContextAccessor) - { - _configurationManager = configurationManager; - } - - /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeOrIgnoreParentalControlSetupRequirement requirement) - { - if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted) - { - context.Succeed(requirement); - return Task.CompletedTask; - } - - var validated = ValidateClaims(context.User, ignoreSchedule: true); - if (validated) - { - context.Succeed(requirement); - } - else - { - context.Fail(); - } - - return Task.CompletedTask; - } - } -} diff --git a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs deleted file mode 100644 index 00aaec334b..0000000000 --- a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy -{ - /// - /// First time setup or ignore parental controls requirement. - /// - public class FirstTimeOrIgnoreParentalControlSetupRequirement : IAuthorizationRequirement - { - } -} diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs deleted file mode 100644 index f7366bd7a9..0000000000 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy -{ - /// - /// The authorization requirement, requiring incomplete first time setup or default privileges, for the authorization handler. - /// - public class FirstTimeSetupOrDefaultRequirement : IAuthorizationRequirement - { - } -} diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs deleted file mode 100644 index 90b76ee99a..0000000000 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Threading.Tasks; -using Jellyfin.Api.Constants; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Library; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; - -namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy -{ - /// - /// Authorization handler for requiring first time setup or elevated privileges. - /// - public class FirstTimeSetupOrElevatedHandler : BaseAuthorizationHandler - { - private readonly IConfigurationManager _configurationManager; - - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public FirstTimeSetupOrElevatedHandler( - IConfigurationManager configurationManager, - IUserManager userManager, - INetworkManager networkManager, - IHttpContextAccessor httpContextAccessor) - : base(userManager, networkManager, httpContextAccessor) - { - _configurationManager = configurationManager; - } - - /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrElevatedRequirement requirement) - { - if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted) - { - context.Succeed(requirement); - return Task.CompletedTask; - } - - var validated = ValidateClaims(context.User); - if (validated && context.User.IsInRole(UserRoles.Administrator)) - { - context.Succeed(requirement); - } - else - { - context.Fail(); - } - - return Task.CompletedTask; - } - } -} diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedRequirement.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedRequirement.cs deleted file mode 100644 index 51ba637b60..0000000000 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedRequirement.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy -{ - /// - /// The authorization requirement, requiring incomplete first time setup or elevated privileges, for the authorization handler. - /// - public class FirstTimeSetupOrElevatedRequirement : IAuthorizationRequirement - { - } -} diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs similarity index 58% rename from Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs rename to Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs index dd0bd4ec2f..302e052a7c 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs @@ -1,38 +1,35 @@ using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy +namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy { /// /// Authorization handler for requiring first time setup or default privileges. /// - public class FirstTimeSetupOrDefaultHandler : BaseAuthorizationHandler + public class FirstTimeSetupHandler : AuthorizationHandler { private readonly IConfigurationManager _configurationManager; + private readonly IUserManager _userManager; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public FirstTimeSetupOrDefaultHandler( + public FirstTimeSetupHandler( IConfigurationManager configurationManager, - IUserManager userManager, - INetworkManager networkManager, - IHttpContextAccessor httpContextAccessor) - : base(userManager, networkManager, httpContextAccessor) + IUserManager userManager) { _configurationManager = configurationManager; + _userManager = userManager; } /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement requirement) + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupRequirement requirement) { if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted) { @@ -40,14 +37,22 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy return Task.CompletedTask; } - var validated = ValidateClaims(context.User); - if (validated) - { - context.Succeed(requirement); - } - else + if (requirement.RequireAdmin && !context.User.IsInRole(UserRoles.Administrator)) { context.Fail(); + return Task.CompletedTask; + } + + if (!requirement.ValidateParentalSchedule) + { + context.Succeed(requirement); + return Task.CompletedTask; + } + + var user = _userManager.GetUserById(context.User.GetUserId()); + if (user.IsParentalScheduleAllowed()) + { + context.Succeed(requirement); } return Task.CompletedTask; diff --git a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupRequirement.cs b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupRequirement.cs new file mode 100644 index 0000000000..8b7a94954e --- /dev/null +++ b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupRequirement.cs @@ -0,0 +1,26 @@ +using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; + +namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy +{ + /// + /// The authorization requirement, requiring incomplete first time setup or default privileges, for the authorization handler. + /// + public class FirstTimeSetupRequirement : DefaultAuthorizationRequirement + { + /// + /// Initializes a new instance of the class. + /// + /// A value indicating whether to ignore parental schedule. + /// A value indicating whether administrator role is required. + public FirstTimeSetupRequirement(bool validateParentalSchedule = false, bool requireAdmin = true) + { + ValidateParentalSchedule = validateParentalSchedule; + RequireAdmin = requireAdmin; + } + + /// + /// Gets a value indicating whether administrator role is required. + /// + public bool RequireAdmin { get; } + } +} diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs deleted file mode 100644 index a7623556a9..0000000000 --- a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Library; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; - -namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy -{ - /// - /// Escape schedule controls handler. - /// - public class IgnoreParentalControlHandler : BaseAuthorizationHandler - { - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public IgnoreParentalControlHandler( - IUserManager userManager, - INetworkManager networkManager, - IHttpContextAccessor httpContextAccessor) - : base(userManager, networkManager, httpContextAccessor) - { - } - - /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement) - { - var validated = ValidateClaims(context.User, ignoreSchedule: true); - if (validated) - { - context.Succeed(requirement); - } - else - { - context.Fail(); - } - - return Task.CompletedTask; - } - } -} diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs deleted file mode 100644 index cdad74270e..0000000000 --- a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy -{ - /// - /// Escape schedule controls requirement. - /// - public class IgnoreParentalControlRequirement : IAuthorizationRequirement - { - } -} diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs deleted file mode 100644 index 14722aa57e..0000000000 --- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Threading.Tasks; -using Jellyfin.Api.Constants; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Library; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; - -namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy -{ - /// - /// Local access or require elevated privileges handler. - /// - public class LocalAccessOrRequiresElevationHandler : BaseAuthorizationHandler - { - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public LocalAccessOrRequiresElevationHandler( - IUserManager userManager, - INetworkManager networkManager, - IHttpContextAccessor httpContextAccessor) - : base(userManager, networkManager, httpContextAccessor) - { - } - - /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessOrRequiresElevationRequirement requirement) - { - var validated = ValidateClaims(context.User, localAccessOnly: true); - if (validated || context.User.IsInRole(UserRoles.Administrator)) - { - context.Succeed(requirement); - } - else - { - context.Fail(); - } - - return Task.CompletedTask; - } - } -} diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs deleted file mode 100644 index d9c64d01c4..0000000000 --- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy -{ - /// - /// The local access or elevated privileges authorization requirement. - /// - public class LocalAccessOrRequiresElevationRequirement : IAuthorizationRequirement - { - } -} diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs deleted file mode 100644 index d772ec5542..0000000000 --- a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Library; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; - -namespace Jellyfin.Api.Auth.LocalAccessPolicy -{ - /// - /// Local access handler. - /// - public class LocalAccessHandler : BaseAuthorizationHandler - { - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public LocalAccessHandler( - IUserManager userManager, - INetworkManager networkManager, - IHttpContextAccessor httpContextAccessor) - : base(userManager, networkManager, httpContextAccessor) - { - } - - /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessRequirement requirement) - { - var validated = ValidateClaims(context.User, localAccessOnly: true); - if (validated) - { - context.Succeed(requirement); - } - else - { - context.Fail(); - } - - return Task.CompletedTask; - } - } -} diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs deleted file mode 100644 index 761127fa40..0000000000 --- a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace Jellyfin.Api.Auth.LocalAccessPolicy -{ - /// - /// The local access authorization requirement. - /// - public class LocalAccessRequirement : IAuthorizationRequirement - { - } -} diff --git a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs deleted file mode 100644 index b235c4b63b..0000000000 --- a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Threading.Tasks; -using Jellyfin.Api.Constants; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Library; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; - -namespace Jellyfin.Api.Auth.RequiresElevationPolicy -{ - /// - /// Authorization handler for requiring elevated privileges. - /// - public class RequiresElevationHandler : BaseAuthorizationHandler - { - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public RequiresElevationHandler( - IUserManager userManager, - INetworkManager networkManager, - IHttpContextAccessor httpContextAccessor) - : base(userManager, networkManager, httpContextAccessor) - { - } - - /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequiresElevationRequirement requirement) - { - var validated = ValidateClaims(context.User); - if (validated && context.User.IsInRole(UserRoles.Administrator)) - { - context.Succeed(requirement); - } - else - { - context.Fail(); - } - - return Task.CompletedTask; - } - } -} diff --git a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationRequirement.cs b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationRequirement.cs deleted file mode 100644 index cfff1cc0c5..0000000000 --- a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationRequirement.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace Jellyfin.Api.Auth.RequiresElevationPolicy -{ - /// - /// The authorization requirement for requiring elevated privileges in the authorization handler. - /// - public class RequiresElevationRequirement : IAuthorizationRequirement - { - } -} diff --git a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs index cdd7d8a52b..5c1029b383 100644 --- a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs +++ b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs @@ -1,19 +1,16 @@ using System.Threading.Tasks; using Jellyfin.Api.Extensions; -using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; -using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.SyncPlay; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy { /// /// Default authorization handler. /// - public class SyncPlayAccessHandler : BaseAuthorizationHandler + public class SyncPlayAccessHandler : AuthorizationHandler { private readonly ISyncPlayManager _syncPlayManager; private readonly IUserManager _userManager; @@ -23,14 +20,9 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy /// /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. public SyncPlayAccessHandler( ISyncPlayManager syncPlayManager, - IUserManager userManager, - INetworkManager networkManager, - IHttpContextAccessor httpContextAccessor) - : base(userManager, networkManager, httpContextAccessor) + IUserManager userManager) { _syncPlayManager = syncPlayManager; _userManager = userManager; @@ -39,27 +31,16 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy /// protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SyncPlayAccessRequirement requirement) { - if (!ValidateClaims(context.User)) - { - context.Fail(); - return Task.CompletedTask; - } - var userId = context.User.GetUserId(); var user = _userManager.GetUserById(userId); if (requirement.RequiredAccess == SyncPlayAccessRequirementType.HasAccess) { - if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups - || user.SyncPlayAccess == SyncPlayUserAccessType.JoinGroups + if (user.SyncPlayAccess is SyncPlayUserAccessType.CreateAndJoinGroups or SyncPlayUserAccessType.JoinGroups || _syncPlayManager.IsUserActive(userId)) { context.Succeed(requirement); } - else - { - context.Fail(); - } } else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.CreateGroup) { @@ -67,10 +48,6 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy { context.Succeed(requirement); } - else - { - context.Fail(); - } } else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.JoinGroup) { @@ -79,10 +56,6 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy { context.Succeed(requirement); } - else - { - context.Fail(); - } } else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.IsInGroup) { @@ -90,14 +63,6 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy { context.Succeed(requirement); } - else - { - context.Fail(); - } - } - else - { - context.Fail(); } return Task.CompletedTask; diff --git a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs index 6fab4c0ad8..220b223b39 100644 --- a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs +++ b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs @@ -1,12 +1,12 @@ -using Jellyfin.Data.Enums; -using Microsoft.AspNetCore.Authorization; +using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; +using Jellyfin.Data.Enums; namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy { /// /// The default authorization requirement. /// - public class SyncPlayAccessRequirement : IAuthorizationRequirement + public class SyncPlayAccessRequirement : DefaultAuthorizationRequirement { /// /// Initializes a new instance of the class. diff --git a/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs new file mode 100644 index 0000000000..c3de7be328 --- /dev/null +++ b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using Jellyfin.Api.Auth.DownloadPolicy; +using Jellyfin.Api.Extensions; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; + +namespace Jellyfin.Api.Auth.UserPermissionPolicy +{ + /// + /// Download authorization handler. + /// + public class UserPermissionHandler : AuthorizationHandler + { + private readonly IUserManager _userManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + public UserPermissionHandler(IUserManager userManager) + { + _userManager = userManager; + } + + /// + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserPermissionRequirement requirement) + { + var user = _userManager.GetUserById(context.User.GetUserId()); + if (user.HasPermission(requirement.RequiredPermission)) + { + context.Succeed(requirement); + } + + return Task.CompletedTask; + } + } +} diff --git a/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs new file mode 100644 index 0000000000..195a611992 --- /dev/null +++ b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs @@ -0,0 +1,26 @@ +using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; +using Jellyfin.Data.Enums; + +namespace Jellyfin.Api.Auth.DownloadPolicy +{ + /// + /// The user permission requirement. + /// + public class UserPermissionRequirement : DefaultAuthorizationRequirement + { + /// + /// Initializes a new instance of the class. + /// + /// The required . + /// Whether to validate the user's parental schedule. + public UserPermissionRequirement(PermissionKind requiredPermission, bool validateParentalSchedule = true) : base(validateParentalSchedule) + { + RequiredPermission = requiredPermission; + } + + /// + /// Gets the required user permission. + /// + public PermissionKind RequiredPermission { get; } + } +} diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs index 5a5a2bf466..adc95e57b2 100644 --- a/Jellyfin.Api/Constants/Policies.cs +++ b/Jellyfin.Api/Constants/Policies.cs @@ -5,11 +5,6 @@ namespace Jellyfin.Api.Constants; /// public static class Policies { - /// - /// Policy name for default authorization. - /// - public const string DefaultAuthorization = "DefaultAuthorization"; - /// /// Policy name for requiring first time setup or elevated privileges. /// diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index 069e7311b8..11933fd97f 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -1,7 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; @@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers; /// The artists controller. /// [Route("Artists")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class ArtistsController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index 573b7069c5..42f072f669 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -3,7 +3,6 @@ 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.ModelBinders; using Jellyfin.Data.Enums; @@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers; /// /// Channels Controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class ChannelsController : BaseJellyfinApiController { private readonly IChannelManager _channelManager; diff --git a/Jellyfin.Api/Controllers/ClientLogController.cs b/Jellyfin.Api/Controllers/ClientLogController.cs index 21c31bc936..2c5dbacbbe 100644 --- a/Jellyfin.Api/Controllers/ClientLogController.cs +++ b/Jellyfin.Api/Controllers/ClientLogController.cs @@ -1,7 +1,6 @@ using System.Net.Mime; using System.Threading.Tasks; using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Models.ClientLogDtos; using MediaBrowser.Controller.ClientEvent; @@ -15,7 +14,7 @@ namespace Jellyfin.Api.Controllers; /// /// Client log controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class ClientLogController : BaseJellyfinApiController { private const int MaxDocumentSize = 1_000_000; diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs index 5a4a9bf079..f9f9be7ce4 100644 --- a/Jellyfin.Api/Controllers/CollectionController.cs +++ b/Jellyfin.Api/Controllers/CollectionController.cs @@ -1,7 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using MediaBrowser.Controller.Collections; @@ -17,7 +16,7 @@ namespace Jellyfin.Api.Controllers; /// The collection controller. /// [Route("Collections")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class CollectionController : BaseJellyfinApiController { private readonly ICollectionManager _collectionManager; diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index d53d7cefd0..9007dfc410 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -19,7 +19,7 @@ namespace Jellyfin.Api.Controllers; /// Configuration Controller. /// [Route("System")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class ConfigurationController : BaseJellyfinApiController { private readonly IServerConfigurationManager _configurationManager; diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs index f7e978bad0..076084c7a3 100644 --- a/Jellyfin.Api/Controllers/DashboardController.cs +++ b/Jellyfin.Api/Controllers/DashboardController.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Net.Mime; using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; using Jellyfin.Api.Models; using MediaBrowser.Common.Plugins; using MediaBrowser.Model.Net; @@ -48,7 +47,7 @@ public class DashboardController : BaseJellyfinApiController [HttpGet("web/ConfigurationPages")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] public ActionResult> GetConfigurationPages( [FromQuery] bool? enableInMainMenu) { diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 49d87a3621..6f0006832b 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; @@ -19,7 +18,7 @@ namespace Jellyfin.Api.Controllers; /// /// Display Preferences Controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class DisplayPreferencesController : BaseJellyfinApiController { private readonly IDisplayPreferencesManager _displayPreferencesManager; diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index b68849171f..4d8b4de24f 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -9,7 +9,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.StreamingDtos; @@ -36,7 +35,7 @@ namespace Jellyfin.Api.Controllers; /// Dynamic hls controller. /// [Route("")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class DynamicHlsController : BaseJellyfinApiController { private const string DefaultVodEncoderPreset = "veryfast"; diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index 2378aada5f..dd64ff9034 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; @@ -18,7 +17,7 @@ namespace Jellyfin.Api.Controllers; /// Filters controller. /// [Route("")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class FilterController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index 28ebe20475..711fb4aef1 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -1,7 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; @@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers; /// /// The genres controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class GenresController : BaseJellyfinApiController { private readonly IUserManager _userManager; @@ -132,7 +131,7 @@ public class GenresController : BaseJellyfinApiController QueryResult<(BaseItem, ItemCounts)> result; if (parentItem is ICollectionFolder parentCollectionFolder && (string.Equals(parentCollectionFolder.CollectionType, CollectionType.Music, StringComparison.Ordinal) - || string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal))) + || string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal))) { result = _libraryManager.GetMusicGenres(query); } diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index 085115e1c8..d7cec865e1 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading.Tasks; using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; @@ -80,7 +79,7 @@ public class HlsSegmentController : BaseJellyfinApiController /// Hls video playlist returned. /// A containing the playlist. [HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesPlaylistFile] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] @@ -106,7 +105,7 @@ public class HlsSegmentController : BaseJellyfinApiController /// Encoding stopped successfully. /// A indicating success. [HttpDelete("Videos/ActiveEncodings")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult StopEncodingProcess( [FromQuery, Required] string deviceId, diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index cc824c65ab..b2adb6a2d6 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -88,7 +88,7 @@ public class ImageController : BaseJellyfinApiController /// User does not have permission to delete the image. /// A . [HttpPost("Users/{userId}/Images/{imageType}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [AcceptsImageFile] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -137,7 +137,7 @@ public class ImageController : BaseJellyfinApiController /// User does not have permission to delete the image. /// A . [HttpPost("Users/{userId}/Images/{imageType}/{index}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [AcceptsImageFile] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -186,7 +186,7 @@ public class ImageController : BaseJellyfinApiController /// User does not have permission to delete the image. /// A . [HttpDelete("Users/{userId}/Images/{imageType}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -230,7 +230,7 @@ public class ImageController : BaseJellyfinApiController /// User does not have permission to delete the image. /// A . [HttpDelete("Users/{userId}/Images/{imageType}/{index}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -432,7 +432,7 @@ public class ImageController : BaseJellyfinApiController /// Item not found. /// The list of image infos on success, or if item not found. [HttpGet("Items/{itemId}/Images")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetItemImageInfos([FromRoute, Required] Guid itemId) @@ -1930,10 +1930,10 @@ public class ImageController : BaseJellyfinApiController } var responseHeaders = new Dictionary - { - { "transferMode.dlna.org", "Interactive" }, - { "realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*" } - }; + { + { "transferMode.dlna.org", "Interactive" }, + { "realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*" } + }; if (!imageInfo.IsLocalFile && item is not null) { diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index 89592bade4..43f09b49a2 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; @@ -22,7 +21,7 @@ namespace Jellyfin.Api.Controllers; /// The instant mix controller. /// [Route("")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class InstantMixController : BaseJellyfinApiController { private readonly IUserManager _userManager; diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs index c2ce4e67e5..b030e74dda 100644 --- a/Jellyfin.Api/Controllers/ItemLookupController.cs +++ b/Jellyfin.Api/Controllers/ItemLookupController.cs @@ -23,7 +23,7 @@ namespace Jellyfin.Api.Controllers; /// Item lookup controller. /// [Route("")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class ItemLookupController : BaseJellyfinApiController { private readonly IProviderManager _providerManager; diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 134974dbe0..97922d5dbc 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -1,7 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; @@ -25,7 +24,7 @@ namespace Jellyfin.Api.Controllers; /// The items controller. /// [Route("")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class ItemsController : BaseJellyfinApiController { private readonly IUserManager _userManager; diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 830f84849e..e1ad87412e 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -95,7 +95,7 @@ public class LibraryController : BaseJellyfinApiController /// Item not found. /// A with the original file. [HttpGet("Items/{itemId}/File")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesFile("video/*", "audio/*")] @@ -116,7 +116,7 @@ public class LibraryController : BaseJellyfinApiController /// Critic reviews returned. /// The list of critic reviews. [HttpGet("Items/{itemId}/CriticReviews")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [Obsolete("This endpoint is obsolete.")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetCriticReviews() @@ -134,7 +134,7 @@ public class LibraryController : BaseJellyfinApiController /// Item not found. /// The item theme songs. [HttpGet("Items/{itemId}/ThemeSongs")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetThemeSongs( @@ -200,7 +200,7 @@ public class LibraryController : BaseJellyfinApiController /// Item not found. /// The item theme videos. [HttpGet("Items/{itemId}/ThemeVideos")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetThemeVideos( @@ -266,7 +266,7 @@ public class LibraryController : BaseJellyfinApiController /// Item not found. /// The item theme videos. [HttpGet("Items/{itemId}/ThemeMedia")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetThemeMedia( [FromRoute, Required] Guid itemId, @@ -321,7 +321,7 @@ public class LibraryController : BaseJellyfinApiController /// Unauthorized access. /// A . [HttpDelete("Items/{itemId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public ActionResult DeleteItem(Guid itemId) @@ -350,7 +350,7 @@ public class LibraryController : BaseJellyfinApiController /// Unauthorized access. /// A . [HttpDelete("Items")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) @@ -392,7 +392,7 @@ public class LibraryController : BaseJellyfinApiController /// Item counts returned. /// Item counts. [HttpGet("Items/Counts")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetItemCounts( [FromQuery] Guid? userId, @@ -426,7 +426,7 @@ public class LibraryController : BaseJellyfinApiController /// Item not found. /// Item parents. [HttpGet("Items/{itemId}/Ancestors")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId) @@ -509,7 +509,7 @@ public class LibraryController : BaseJellyfinApiController /// A . [HttpPost("Library/Series/Added", Name = "PostAddedSeries")] [HttpPost("Library/Series/Updated")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostUpdatedSeries([FromQuery] string? tvdbId) { @@ -539,7 +539,7 @@ public class LibraryController : BaseJellyfinApiController /// A . [HttpPost("Library/Movies/Added", Name = "PostAddedMovies")] [HttpPost("Library/Movies/Updated")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostUpdatedMovies([FromQuery] string? tmdbId, [FromQuery] string? imdbId) { @@ -580,7 +580,7 @@ public class LibraryController : BaseJellyfinApiController /// Report success. /// A . [HttpPost("Library/Media/Updated")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto dto) { @@ -657,7 +657,7 @@ public class LibraryController : BaseJellyfinApiController [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows")] [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies")] [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSimilarItems( [FromRoute, Required] Guid itemId, @@ -802,32 +802,32 @@ public class LibraryController : BaseJellyfinApiController Type = type, MetadataFetchers = plugins - .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) - .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataFetcher)) - .Select(i => new LibraryOptionInfoDto - { - Name = i.Name, - DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary) - }) - .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .ToArray(), + .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) + .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataFetcher)) + .Select(i => new LibraryOptionInfoDto + { + Name = i.Name, + DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary) + }) + .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) + .ToArray(), ImageFetchers = plugins - .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) - .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.ImageFetcher)) - .Select(i => new LibraryOptionInfoDto - { - Name = i.Name, - DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary) - }) - .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .ToArray(), + .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) + .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.ImageFetcher)) + .Select(i => new LibraryOptionInfoDto + { + Name = i.Name, + DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary) + }) + .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) + .ToArray(), SupportedImageTypes = plugins - .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) - .SelectMany(i => i.SupportedImageTypes ?? Array.Empty()) - .Distinct() - .ToArray(), + .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) + .SelectMany(i => i.SupportedImageTypes ?? Array.Empty()) + .Distinct() + .ToArray(), DefaultImageOptions = defaultImageOptions ?? Array.Empty() }); @@ -920,13 +920,13 @@ public class LibraryController : BaseJellyfinApiController if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase)) { return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase) - || string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase) - || string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase)); + || string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase) + || string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase)); } return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase) - || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase) - || string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase); + || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase); } var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions @@ -934,7 +934,7 @@ public class LibraryController : BaseJellyfinApiController .ToArray(); return metadataOptions.Length == 0 - || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase)); + || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase)); } private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary) diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 21b4243464..c1f5d74cd9 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -10,7 +10,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; @@ -95,7 +94,7 @@ public class LiveTvController : BaseJellyfinApiController /// [HttpGet("Info")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] public ActionResult GetLiveTvInfo() { return _liveTvManager.GetLiveTvInfo(CancellationToken.None); @@ -131,7 +130,7 @@ public class LiveTvController : BaseJellyfinApiController /// [HttpGet("Channels")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] public ActionResult> GetLiveTvChannels( [FromQuery] ChannelType? type, [FromQuery] Guid? userId, @@ -210,7 +209,7 @@ public class LiveTvController : BaseJellyfinApiController /// An containing the live tv channel. [HttpGet("Channels/{channelId}")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] public ActionResult GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId) { var user = userId is null || userId.Value.Equals(default) @@ -251,7 +250,7 @@ public class LiveTvController : BaseJellyfinApiController /// An containing the live tv recordings. [HttpGet("Recordings")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] public ActionResult> GetRecordings( [FromQuery] string? channelId, [FromQuery] Guid? userId, @@ -322,7 +321,7 @@ public class LiveTvController : BaseJellyfinApiController /// An containing the live tv recordings. [HttpGet("Recordings/Series")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [Obsolete("This endpoint is obsolete.")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] @@ -365,7 +364,7 @@ public class LiveTvController : BaseJellyfinApiController /// An containing the recording groups. [HttpGet("Recordings/Groups")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [Obsolete("This endpoint is obsolete.")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] public ActionResult> GetRecordingGroups([FromQuery] Guid? userId) @@ -381,7 +380,7 @@ public class LiveTvController : BaseJellyfinApiController /// An containing the recording folders. [HttpGet("Recordings/Folders")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] public ActionResult> GetRecordingFolders([FromQuery] Guid? userId) { var user = userId is null || userId.Value.Equals(default) @@ -403,7 +402,7 @@ public class LiveTvController : BaseJellyfinApiController /// An containing the live tv recording. [HttpGet("Recordings/{recordingId}")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] public ActionResult GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId) { var user = userId is null || userId.Value.Equals(default) @@ -425,7 +424,7 @@ public class LiveTvController : BaseJellyfinApiController /// A . [HttpPost("Tuners/{tunerId}/Reset")] [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] public async Task ResetTuner([FromRoute, Required] string tunerId) { await AssertUserCanManageLiveTv().ConfigureAwait(false); @@ -443,7 +442,7 @@ public class LiveTvController : BaseJellyfinApiController /// [HttpGet("Timers/{timerId}")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] public async Task> GetTimer([FromRoute, Required] string timerId) { return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false); @@ -459,7 +458,7 @@ public class LiveTvController : BaseJellyfinApiController /// [HttpGet("Timers/Defaults")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] public async Task> GetDefaultTimer([FromQuery] string? programId) { return string.IsNullOrEmpty(programId) @@ -479,7 +478,7 @@ public class LiveTvController : BaseJellyfinApiController /// [HttpGet("Timers")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] public async Task>> GetTimers( [FromQuery] string? channelId, [FromQuery] string? seriesTimerId, @@ -533,7 +532,7 @@ public class LiveTvController : BaseJellyfinApiController /// [HttpGet("Programs")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] public async Task>> GetLiveTvPrograms( [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds, [FromQuery] Guid? userId, @@ -616,7 +615,7 @@ public class LiveTvController : BaseJellyfinApiController /// [HttpPost("Programs")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] public async Task>> GetPrograms([FromBody] GetProgramsDto body) { var user = body.UserId.Equals(default) ? null : _userManager.GetUserById(body.UserId); @@ -682,7 +681,7 @@ public class LiveTvController : BaseJellyfinApiController /// Recommended epgs returned. /// A containing the queryresult of recommended epgs. [HttpGet("Programs/Recommended")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetRecommendedPrograms( [FromQuery] Guid? userId, @@ -734,7 +733,7 @@ public class LiveTvController : BaseJellyfinApiController /// Program returned. /// An containing the livetv program. [HttpGet("Programs/{programId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetProgram( [FromRoute, Required] string programId, @@ -755,7 +754,7 @@ public class LiveTvController : BaseJellyfinApiController /// Item not found. /// A on success, or a if item not found. [HttpDelete("Recordings/{recordingId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DeleteRecording([FromRoute, Required] Guid recordingId) @@ -783,7 +782,7 @@ public class LiveTvController : BaseJellyfinApiController /// Timer deleted. /// A . [HttpDelete("Timers/{timerId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task CancelTimer([FromRoute, Required] string timerId) { @@ -800,7 +799,7 @@ public class LiveTvController : BaseJellyfinApiController /// Timer updated. /// A . [HttpPost("Timers/{timerId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")] public async Task UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo) @@ -817,7 +816,7 @@ public class LiveTvController : BaseJellyfinApiController /// Timer created. /// A . [HttpPost("Timers")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task CreateTimer([FromBody] TimerInfoDto timerInfo) { @@ -834,7 +833,7 @@ public class LiveTvController : BaseJellyfinApiController /// Series timer not found. /// A on success, or a if timer not found. [HttpGet("SeriesTimers/{timerId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetSeriesTimer([FromRoute, Required] string timerId) @@ -856,7 +855,7 @@ public class LiveTvController : BaseJellyfinApiController /// Timers returned. /// An of live tv series timers. [HttpGet("SeriesTimers")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetSeriesTimers([FromQuery] string? sortBy, [FromQuery] SortOrder? sortOrder) { @@ -876,7 +875,7 @@ public class LiveTvController : BaseJellyfinApiController /// Timer cancelled. /// A . [HttpDelete("SeriesTimers/{timerId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task CancelSeriesTimer([FromRoute, Required] string timerId) { @@ -893,7 +892,7 @@ public class LiveTvController : BaseJellyfinApiController /// Series timer updated. /// A . [HttpPost("SeriesTimers/{timerId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")] public async Task UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo) @@ -910,7 +909,7 @@ public class LiveTvController : BaseJellyfinApiController /// Series timer info created. /// A . [HttpPost("SeriesTimers")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo) { @@ -925,7 +924,7 @@ public class LiveTvController : BaseJellyfinApiController /// Group id. /// A . [HttpGet("Recordings/Groups/{groupId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status404NotFound)] [Obsolete("This endpoint is obsolete.")] public ActionResult GetRecordingGroup([FromRoute, Required] Guid groupId) @@ -939,7 +938,7 @@ public class LiveTvController : BaseJellyfinApiController /// Guid info returned. /// An containing the guide info. [HttpGet("GuideInfo")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetGuideInfo() { @@ -953,7 +952,7 @@ public class LiveTvController : BaseJellyfinApiController /// Created tuner host returned. /// A containing the created tuner host. [HttpPost("TunerHosts")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo) { @@ -967,7 +966,7 @@ public class LiveTvController : BaseJellyfinApiController /// Tuner host deleted. /// A . [HttpDelete("TunerHosts")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult DeleteTunerHost([FromQuery] string? id) { @@ -983,7 +982,7 @@ public class LiveTvController : BaseJellyfinApiController /// Default listings provider info returned. /// An containing the default listings provider info. [HttpGet("ListingProviders/Default")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetDefaultListingProvider() { @@ -1000,7 +999,7 @@ public class LiveTvController : BaseJellyfinApiController /// Created listings provider returned. /// A containing the created listings provider. [HttpPost("ListingProviders")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")] public async Task> AddListingProvider( @@ -1026,7 +1025,7 @@ public class LiveTvController : BaseJellyfinApiController /// Listing provider deleted. /// A . [HttpDelete("ListingProviders")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult DeleteListingProvider([FromQuery] string? id) { @@ -1044,7 +1043,7 @@ public class LiveTvController : BaseJellyfinApiController /// Available lineups returned. /// A containing the available lineups. [HttpGet("ListingProviders/Lineups")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetLineups( [FromQuery] string? id, @@ -1061,7 +1060,7 @@ public class LiveTvController : BaseJellyfinApiController /// Available countries returned. /// A containing the available countries. [HttpGet("ListingProviders/SchedulesDirect/Countries")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesFile(MediaTypeNames.Application.Json)] public async Task GetSchedulesDirectCountries() @@ -1082,7 +1081,7 @@ public class LiveTvController : BaseJellyfinApiController /// Channel mapping options returned. /// An containing the channel mapping options. [HttpGet("ChannelMappingOptions")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetChannelMappingOptions([FromQuery] string? providerId) { @@ -1120,7 +1119,7 @@ public class LiveTvController : BaseJellyfinApiController /// Created channel mapping returned. /// An containing the created channel mapping. [HttpPost("ChannelMappings")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto) { @@ -1133,7 +1132,7 @@ public class LiveTvController : BaseJellyfinApiController /// Tuner host types returned. /// An containing the tuner host types. [HttpGet("TunerHosts/Types")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetTunerHostTypes() { @@ -1148,7 +1147,7 @@ public class LiveTvController : BaseJellyfinApiController /// An containing the tuners. [HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")] [HttpGet("Tuners/Discover")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> DiscoverTuners([FromQuery] bool newDevicesOnly = false) { diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index eee7df3af8..ea10dd771f 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Net.Mime; using System.Threading.Tasks; using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.MediaInfoDtos; @@ -25,7 +24,7 @@ namespace Jellyfin.Api.Controllers; /// The media info controller. /// [Route("")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class MediaInfoController : BaseJellyfinApiController { private readonly IMediaSourceManager _mediaSourceManager; diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index 4c30dd2b37..a9336f6d24 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; @@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers; /// /// Movies controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class MoviesController : BaseJellyfinApiController { private readonly IUserManager _userManager; diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 302f138ebc..da1a6e832c 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -1,7 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; @@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers; /// /// The music genres controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class MusicGenresController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 3cb3caadb3..0ba5e995fb 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -17,7 +17,7 @@ namespace Jellyfin.Api.Controllers; /// Package Controller. /// [Route("")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class PackageController : BaseJellyfinApiController { private readonly IInstallationManager _installationManager; diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index 9fb6da5276..5310f50b13 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -1,7 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; @@ -20,7 +19,7 @@ namespace Jellyfin.Api.Controllers; /// /// Persons controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class PersonsController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 11e5893017..79c0d3c7b2 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -4,7 +4,6 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.PlaylistDtos; @@ -25,7 +24,7 @@ namespace Jellyfin.Api.Controllers; /// /// Playlists controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class PlaylistsController : BaseJellyfinApiController { private readonly IPlaylistManager _playlistManager; diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs index 18d6ebf1e0..11f3ddbb0a 100644 --- a/Jellyfin.Api/Controllers/PlaystateController.cs +++ b/Jellyfin.Api/Controllers/PlaystateController.cs @@ -2,7 +2,6 @@ using System; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; @@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers; /// Playstate controller. /// [Route("")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class PlaystateController : BaseJellyfinApiController { private readonly IUserManager _userManager; diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 5a037d7a6b..4726cf0663 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -21,7 +21,7 @@ namespace Jellyfin.Api.Controllers; /// /// Plugins controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class PluginsController : BaseJellyfinApiController { private readonly IInstallationManager _installationManager; diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs index a58e85b2b2..503b9d3729 100644 --- a/Jellyfin.Api/Controllers/QuickConnectController.cs +++ b/Jellyfin.Api/Controllers/QuickConnectController.cs @@ -111,7 +111,7 @@ public class QuickConnectController : BaseJellyfinApiController /// Unknown user id. /// Boolean indicating if the authorization was successful. [HttpPost("Authorize")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task> AuthorizeQuickConnect([FromQuery, Required] string code, [FromQuery] Guid? userId = null) diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs index 445c5594f8..5c77db2407 100644 --- a/Jellyfin.Api/Controllers/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -56,7 +56,7 @@ public class RemoteImageController : BaseJellyfinApiController /// Item not found. /// Remote Image Result. [HttpGet("Items/{itemId}/RemoteImages")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetRemoteImages( @@ -121,7 +121,7 @@ public class RemoteImageController : BaseJellyfinApiController /// Item not found. /// List of remote image providers. [HttpGet("Items/{itemId}/RemoteImages/Providers")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetRemoteImageProviders([FromRoute, Required] Guid itemId) diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 46b4920cad..a25b43345a 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -3,7 +3,6 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using Jellyfin.Extensions; @@ -26,7 +25,7 @@ namespace Jellyfin.Api.Controllers; /// Search controller. /// [Route("Search/Hints")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class SearchController : BaseJellyfinApiController { private readonly ISearchEngine _searchEngine; diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index ef33644785..bae8e0a490 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -56,7 +56,7 @@ public class SessionController : BaseJellyfinApiController /// List of sessions returned. /// An with the available sessions. [HttpGet("Sessions")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSessions( [FromQuery] Guid? controllableByUserId, @@ -119,7 +119,7 @@ public class SessionController : BaseJellyfinApiController /// Instruction sent to session. /// A . [HttpPost("Sessions/{sessionId}/Viewing")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task DisplayContent( [FromRoute, Required] string sessionId, @@ -158,7 +158,7 @@ public class SessionController : BaseJellyfinApiController /// Instruction sent to session. /// A . [HttpPost("Sessions/{sessionId}/Playing")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task Play( [FromRoute, Required] string sessionId, @@ -201,7 +201,7 @@ public class SessionController : BaseJellyfinApiController /// Playstate command sent to session. /// A . [HttpPost("Sessions/{sessionId}/Playing/{command}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task SendPlaystateCommand( [FromRoute, Required] string sessionId, @@ -232,7 +232,7 @@ public class SessionController : BaseJellyfinApiController /// System command sent to session. /// A . [HttpPost("Sessions/{sessionId}/System/{command}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task SendSystemCommand( [FromRoute, Required] string sessionId, @@ -258,7 +258,7 @@ public class SessionController : BaseJellyfinApiController /// General command sent to session. /// A . [HttpPost("Sessions/{sessionId}/Command/{command}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task SendGeneralCommand( [FromRoute, Required] string sessionId, @@ -286,7 +286,7 @@ public class SessionController : BaseJellyfinApiController /// Full general command sent to session. /// A . [HttpPost("Sessions/{sessionId}/Command")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task SendFullGeneralCommand( [FromRoute, Required] string sessionId, @@ -316,7 +316,7 @@ public class SessionController : BaseJellyfinApiController /// Message sent. /// A . [HttpPost("Sessions/{sessionId}/Message")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task SendMessageCommand( [FromRoute, Required] string sessionId, @@ -345,7 +345,7 @@ public class SessionController : BaseJellyfinApiController /// User added to session. /// A . [HttpPost("Sessions/{sessionId}/User/{userId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult AddUserToSession( [FromRoute, Required] string sessionId, @@ -363,7 +363,7 @@ public class SessionController : BaseJellyfinApiController /// User removed from session. /// A . [HttpDelete("Sessions/{sessionId}/User/{userId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult RemoveUserFromSession( [FromRoute, Required] string sessionId, @@ -385,7 +385,7 @@ public class SessionController : BaseJellyfinApiController /// Capabilities posted. /// A . [HttpPost("Sessions/Capabilities")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task PostCapabilities( [FromQuery] string? id, @@ -419,7 +419,7 @@ public class SessionController : BaseJellyfinApiController /// Capabilities updated. /// A . [HttpPost("Sessions/Capabilities/Full")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task PostFullCapabilities( [FromQuery] string? id, @@ -443,7 +443,7 @@ public class SessionController : BaseJellyfinApiController /// Session reported to server. /// A . [HttpPost("Sessions/Viewing")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task ReportViewing( [FromQuery] string? sessionId, @@ -461,7 +461,7 @@ public class SessionController : BaseJellyfinApiController /// Session end reported to server. /// A . [HttpPost("Sessions/Logout")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task ReportSessionEnded() { diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs index 799be2ae86..21965e956d 100644 --- a/Jellyfin.Api/Controllers/StudiosController.cs +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -1,6 +1,5 @@ using System; using System.ComponentModel.DataAnnotations; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; @@ -21,7 +20,7 @@ namespace Jellyfin.Api.Controllers; /// /// Studios controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class StudiosController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index fd0a71f9e3..e384213380 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -114,7 +114,7 @@ public class SubtitleController : BaseJellyfinApiController /// Subtitles retrieved. /// An array of . [HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> SearchRemoteSubtitles( [FromRoute, Required] Guid itemId, @@ -134,7 +134,7 @@ public class SubtitleController : BaseJellyfinApiController /// Subtitle downloaded. /// A . [HttpPost("Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task DownloadRemoteSubtitles( [FromRoute, Required] Guid itemId, @@ -164,7 +164,7 @@ public class SubtitleController : BaseJellyfinApiController /// File returned. /// A with the subtitle file. [HttpGet("Providers/Subtitles/Subtitles/{id}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [Produces(MediaTypeNames.Application.Octet)] [ProducesFile("text/*")] @@ -322,7 +322,7 @@ public class SubtitleController : BaseJellyfinApiController /// Subtitle playlist retrieved. /// A with the HLS subtitle playlist. [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesPlaylistFile] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] @@ -463,7 +463,7 @@ public class SubtitleController : BaseJellyfinApiController /// Information retrieved. /// An array of with the available font files. [HttpGet("FallbackFont/Fonts")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public IEnumerable GetFallbackFontList() { @@ -514,7 +514,7 @@ public class SubtitleController : BaseJellyfinApiController /// Fallback font file retrieved. /// The fallback font file. [HttpGet("FallbackFont/Fonts/{name}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesFile("font/*")] public ActionResult GetFallbackFont([FromRoute, Required] string name) diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index c5c429757a..5b808f257c 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -1,6 +1,5 @@ using System; using System.ComponentModel.DataAnnotations; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; @@ -19,7 +18,7 @@ namespace Jellyfin.Api.Controllers; /// The suggestions controller. /// [Route("")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class SuggestionsController : BaseJellyfinApiController { private readonly IDtoService _dtoService; diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index b0b2e2d6d8..4ab705f40a 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -172,7 +172,7 @@ public class SystemController : BaseJellyfinApiController /// Information retrieved. /// with information about the endpoint. [HttpGet("Endpoint")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetEndpointInfo() { @@ -210,7 +210,7 @@ public class SystemController : BaseJellyfinApiController /// Information retrieved. /// An with the WakeOnLan infos. [HttpGet("WakeOnLanInfo")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [Obsolete("This endpoint is obsolete.")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetWakeOnLanInfo() diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index 115efcd8f3..b5b6406206 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -1,5 +1,4 @@ using System; -using Jellyfin.Api.Constants; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using MediaBrowser.Model.Dto; @@ -14,7 +13,7 @@ namespace Jellyfin.Api.Controllers; /// /// The trailers controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class TrailersController : BaseJellyfinApiController { private readonly ItemsController _itemsController; diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 2be32095e3..b0760f97c7 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; @@ -25,7 +24,7 @@ namespace Jellyfin.Api.Controllers; /// The tv shows controller. /// [Route("Shows")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class TvShowsController : BaseJellyfinApiController { private readonly IUserManager _userManager; diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 6946caa2ba..3455215979 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -5,7 +5,6 @@ using System.Globalization; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; @@ -82,7 +81,7 @@ public class UniversalAudioController : BaseJellyfinApiController /// A containing the audio file. [HttpGet("Audio/{itemId}/universal")] [HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status302Found)] [ProducesAudioFile] diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 7f184f31e7..d9ea96f2d7 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -81,7 +81,7 @@ public class UserController : BaseJellyfinApiController /// Users returned. /// An containing the users. [HttpGet] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetUsers( [FromQuery] bool? isHidden, @@ -251,7 +251,7 @@ public class UserController : BaseJellyfinApiController /// User not found. /// A indicating success or a or a on failure. [HttpPost("{userId}/Password")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -312,7 +312,7 @@ public class UserController : BaseJellyfinApiController /// User not found. /// A indicating success or a or a on failure. [HttpPost("{userId}/EasyPassword")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -354,7 +354,7 @@ public class UserController : BaseJellyfinApiController /// User update forbidden. /// A indicating success or a or a on failure. [HttpPost("{userId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -440,7 +440,7 @@ public class UserController : BaseJellyfinApiController /// User configuration update forbidden. /// A indicating success. [HttpPost("{userId}/Configuration")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task UpdateUserConfiguration( @@ -526,7 +526,7 @@ public class UserController : BaseJellyfinApiController /// Token is not owned by a user. /// A for the authenticated user. [HttpGet("Me")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public ActionResult GetCurrentUser() diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index 556cf38945..93312b817f 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -4,7 +4,6 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; @@ -28,7 +27,7 @@ namespace Jellyfin.Api.Controllers; /// User library controller. /// [Route("")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class UserLibraryController : BaseJellyfinApiController { private readonly IUserManager _userManager; diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index aa7ba8891b..838b432340 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.UserViewDtos; @@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers; /// User views controller. /// [Route("")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class UserViewsController : BaseJellyfinApiController { private readonly IUserManager _userManager; diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 01a319879a..9299991966 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -100,7 +100,7 @@ public class VideosController : BaseJellyfinApiController /// Additional parts returned. /// A with the parts. [HttpGet("{itemId}/AdditionalParts")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetAdditionalPart([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId) { diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index 2e5fdc1464..def37cb971 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; @@ -24,7 +23,7 @@ namespace Jellyfin.Api.Controllers; /// /// Years controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class YearsController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs index 7b05351e32..9c2194fafd 100644 --- a/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs +++ b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs @@ -1,6 +1,6 @@ -using System.Net; using System.Threading.Tasks; using Jellyfin.Networking.Configuration; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Http; @@ -32,9 +32,14 @@ public class LanFilteringMiddleware /// The async task. public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager) { - var host = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback; + if (serverConfigurationManager.GetNetworkConfiguration().EnableRemoteAccess) + { + await _next(httpContext).ConfigureAwait(false); + return; + } - if (!networkManager.IsInLocalNetwork(host) && !serverConfigurationManager.GetNetworkConfiguration().EnableRemoteAccess) + var host = httpContext.GetNormalizedRemoteIp(); + if (!networkManager.IsInLocalNetwork(host)) { return; } diff --git a/Jellyfin.Data/DayOfWeekHelper.cs b/Jellyfin.Data/DayOfWeekHelper.cs index b7ba30180e..d1ce8185f2 100644 --- a/Jellyfin.Data/DayOfWeekHelper.cs +++ b/Jellyfin.Data/DayOfWeekHelper.cs @@ -17,5 +17,16 @@ namespace Jellyfin.Data _ => new[] { (DayOfWeek)day } }; } + + public static bool Contains(this DynamicDayOfWeek dynamicDayOfWeek, DayOfWeek dayOfWeek) + { + return dynamicDayOfWeek switch + { + DynamicDayOfWeek.Everyday => true, + DynamicDayOfWeek.Weekday => dayOfWeek is > DayOfWeek.Sunday and <= DayOfWeek.Friday, + DynamicDayOfWeek.Weekend => dayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday, + _ => (DayOfWeek)dynamicDayOfWeek == dayOfWeek + }; + } } } diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs index eb59e70f3b..4ce581749f 100644 --- a/Jellyfin.Data/Entities/User.cs +++ b/Jellyfin.Data/Entities/User.cs @@ -525,8 +525,9 @@ namespace Jellyfin.Data.Entities { var localTime = date.ToLocalTime(); var hour = localTime.TimeOfDay.TotalHours; + var currentDayOfWeek = localTime.DayOfWeek; - return DayOfWeekHelper.GetDaysOfWeek(schedule.DayOfWeek).Contains(localTime.DayOfWeek) + return schedule.DayOfWeek.Contains(currentDayOfWeek) && hour >= schedule.StartHour && hour <= schedule.EndHour; } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index e9af1cf83c..e2dcaf5f5c 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -5,19 +5,15 @@ using System.Linq; using System.Net; using System.Net.Sockets; using System.Reflection; +using System.Security.Claims; using Emby.Server.Implementations; using Jellyfin.Api.Auth; using Jellyfin.Api.Auth.AnonymousLanAccessPolicy; using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Auth.DownloadPolicy; -using Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy; -using Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy; -using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; -using Jellyfin.Api.Auth.IgnoreParentalControlPolicy; -using Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy; -using Jellyfin.Api.Auth.LocalAccessPolicy; -using Jellyfin.Api.Auth.RequiresElevationPolicy; +using Jellyfin.Api.Auth.FirstTimeSetupPolicy; using Jellyfin.Api.Auth.SyncPlayAccessPolicy; +using Jellyfin.Api.Auth.UserPermissionPolicy; using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; using Jellyfin.Api.Formatters; @@ -56,117 +52,34 @@ namespace Jellyfin.Server.Extensions /// The updated service collection. public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection) { + // The default handler must be first so that it is evaluated first serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + return serviceCollection.AddAuthorizationCore(options => { - options.AddPolicy( - Policies.DefaultAuthorization, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new DefaultAuthorizationRequirement()); - }); - options.AddPolicy( - Policies.Download, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new DownloadRequirement()); - }); - options.AddPolicy( - Policies.FirstTimeSetupOrDefault, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new FirstTimeSetupOrDefaultRequirement()); - }); - options.AddPolicy( - Policies.FirstTimeSetupOrElevated, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new FirstTimeSetupOrElevatedRequirement()); - }); - options.AddPolicy( - Policies.IgnoreParentalControl, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new IgnoreParentalControlRequirement()); - }); - options.AddPolicy( - Policies.FirstTimeSetupOrIgnoreParentalControl, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new FirstTimeOrIgnoreParentalControlSetupRequirement()); - }); - options.AddPolicy( - Policies.LocalAccessOnly, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new LocalAccessRequirement()); - }); - options.AddPolicy( - Policies.LocalAccessOrRequiresElevation, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new LocalAccessOrRequiresElevationRequirement()); - }); + options.DefaultPolicy = new AuthorizationPolicyBuilder() + .AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication) + .RequireAuthenticatedUser() + .Build(); + + options.AddPolicy(Policies.Download, new UserPermissionRequirement(PermissionKind.EnableContentDownloading)); + options.AddPolicy(Policies.FirstTimeSetupOrDefault, new FirstTimeSetupRequirement()); + options.AddPolicy(Policies.FirstTimeSetupOrElevated, new FirstTimeSetupRequirement(requireAdmin: true)); + options.AddPolicy(Policies.FirstTimeSetupOrIgnoreParentalControl, new FirstTimeSetupRequirement(validateParentalSchedule: false)); + options.AddPolicy(Policies.IgnoreParentalControl, new DefaultAuthorizationRequirement(validateParentalSchedule: false)); + options.AddPolicy(Policies.SyncPlayHasAccess, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.HasAccess)); + options.AddPolicy(Policies.SyncPlayCreateGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup)); + options.AddPolicy(Policies.SyncPlayJoinGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup)); + options.AddPolicy(Policies.SyncPlayIsInGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup)); + options.AddPolicy(Policies.AnonymousLanAccessPolicy, new AnonymousLanAccessRequirement()); options.AddPolicy( Policies.RequiresElevation, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new RequiresElevationRequirement()); - }); - options.AddPolicy( - Policies.SyncPlayHasAccess, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.HasAccess)); - }); - options.AddPolicy( - Policies.SyncPlayCreateGroup, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup)); - }); - options.AddPolicy( - Policies.SyncPlayJoinGroup, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup)); - }); - options.AddPolicy( - Policies.SyncPlayIsInGroup, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup)); - }); - options.AddPolicy( - Policies.AnonymousLanAccessPolicy, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new AnonymousLanAccessRequirement()); - }); + policy => policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication) + .RequireClaim(ClaimTypes.Role, UserRoles.Administrator)); }); } @@ -334,6 +247,14 @@ namespace Jellyfin.Server.Extensions }); } + private static void AddPolicy(this AuthorizationOptions authorizationOptions, string policyName, IAuthorizationRequirement authorizationRequirement) + { + authorizationOptions.AddPolicy(policyName, policy => + { + policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication).AddRequirements(authorizationRequirement); + }); + } + /// /// Sets up the proxy configuration based on the addresses in . /// diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs similarity index 80% rename from tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs rename to tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs index ee42216e46..6669a6689c 100644 --- a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs @@ -2,7 +2,8 @@ using System.Collections.Generic; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; -using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; +using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; +using Jellyfin.Api.Auth.FirstTimeSetupPolicy; using Jellyfin.Api.Constants; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Library; @@ -11,25 +12,25 @@ using Microsoft.AspNetCore.Http; using Moq; using Xunit; -namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy +namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupPolicy { - public class FirstTimeSetupOrElevatedHandlerTests + public class FirstTimeSetupHandlerTests { private readonly Mock _configurationManagerMock; private readonly List _requirements; - private readonly FirstTimeSetupOrElevatedHandler _sut; + private readonly FirstTimeSetupHandler _firstTimeSetupHandler; private readonly Mock _userManagerMock; private readonly Mock _httpContextAccessor; - public FirstTimeSetupOrElevatedHandlerTests() + public FirstTimeSetupHandlerTests() { var fixture = new Fixture().Customize(new AutoMoqCustomization()); _configurationManagerMock = fixture.Freeze>(); - _requirements = new List { new FirstTimeSetupOrElevatedRequirement() }; + _requirements = new List { new FirstTimeSetupRequirement() }; _userManagerMock = fixture.Freeze>(); _httpContextAccessor = fixture.Freeze>(); - _sut = fixture.Create(); + _firstTimeSetupHandler = fixture.Create(); } [Theory] @@ -46,7 +47,7 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy var context = new AuthorizationHandlerContext(_requirements, claims, null); - await _sut.HandleAsync(context); + await _firstTimeSetupHandler.HandleAsync(context); Assert.True(context.HasSucceeded); } @@ -64,7 +65,7 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy var context = new AuthorizationHandlerContext(_requirements, claims, null); - await _sut.HandleAsync(context); + await _firstTimeSetupHandler.HandleAsync(context); Assert.Equal(shouldSucceed, context.HasSucceeded); } } diff --git a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs index 7150c90bb8..9cf8f85483 100644 --- a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; -using Jellyfin.Api.Auth.IgnoreParentalControlPolicy; +using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Constants; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; @@ -20,7 +20,7 @@ namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy { private readonly Mock _configurationManagerMock; private readonly List _requirements; - private readonly IgnoreParentalControlHandler _sut; + private readonly DefaultAuthorizationHandler _sut; private readonly Mock _userManagerMock; private readonly Mock _httpContextAccessor; @@ -33,11 +33,11 @@ namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy { var fixture = new Fixture().Customize(new AutoMoqCustomization()); _configurationManagerMock = fixture.Freeze>(); - _requirements = new List { new IgnoreParentalControlRequirement() }; + _requirements = new List { new DefaultAuthorizationRequirement(validateParentalSchedule: false) }; _userManagerMock = fixture.Freeze>(); _httpContextAccessor = fixture.Freeze>(); - _sut = fixture.Create(); + _sut = fixture.Create(); } [Theory] diff --git a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs deleted file mode 100644 index 5b3d784ffa..0000000000 --- a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; -using AutoFixture; -using AutoFixture.AutoMoq; -using Jellyfin.Api.Auth.LocalAccessPolicy; -using Jellyfin.Api.Constants; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Library; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Moq; -using Xunit; - -namespace Jellyfin.Api.Tests.Auth.LocalAccessPolicy -{ - public class LocalAccessHandlerTests - { - private readonly Mock _configurationManagerMock; - private readonly List _requirements; - private readonly LocalAccessHandler _sut; - private readonly Mock _userManagerMock; - private readonly Mock _httpContextAccessor; - private readonly Mock _networkManagerMock; - - public LocalAccessHandlerTests() - { - var fixture = new Fixture().Customize(new AutoMoqCustomization()); - _configurationManagerMock = fixture.Freeze>(); - _requirements = new List { new LocalAccessRequirement() }; - _userManagerMock = fixture.Freeze>(); - _httpContextAccessor = fixture.Freeze>(); - _networkManagerMock = fixture.Freeze>(); - - _sut = fixture.Create(); - } - - [Theory] - [InlineData(true, true)] - [InlineData(false, false)] - public async Task LocalAccessOnly(bool isInLocalNetwork, bool shouldSucceed) - { - _networkManagerMock - .Setup(n => n.IsInLocalNetwork(It.IsAny())) - .Returns(isInLocalNetwork); - - TestHelpers.SetupConfigurationManager(_configurationManagerMock, true); - var claims = TestHelpers.SetupUser( - _userManagerMock, - _httpContextAccessor, - UserRoles.User); - - var context = new AuthorizationHandlerContext(_requirements, claims, null); - await _sut.HandleAsync(context); - Assert.Equal(shouldSucceed, context.HasSucceeded); - } - } -} diff --git a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs deleted file mode 100644 index ffe88fcdeb..0000000000 --- a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using AutoFixture; -using AutoFixture.AutoMoq; -using Jellyfin.Api.Auth.RequiresElevationPolicy; -using Jellyfin.Api.Constants; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Library; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Moq; -using Xunit; - -namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy -{ - public class RequiresElevationHandlerTests - { - private readonly Mock _configurationManagerMock; - private readonly List _requirements; - private readonly RequiresElevationHandler _sut; - private readonly Mock _userManagerMock; - private readonly Mock _httpContextAccessor; - - public RequiresElevationHandlerTests() - { - var fixture = new Fixture().Customize(new AutoMoqCustomization()); - _configurationManagerMock = fixture.Freeze>(); - _requirements = new List { new RequiresElevationRequirement() }; - _userManagerMock = fixture.Freeze>(); - _httpContextAccessor = fixture.Freeze>(); - - _sut = fixture.Create(); - } - - [Theory] - [InlineData(UserRoles.Administrator, true)] - [InlineData(UserRoles.User, false)] - [InlineData(UserRoles.Guest, false)] - public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed) - { - TestHelpers.SetupConfigurationManager(_configurationManagerMock, true); - var claims = TestHelpers.SetupUser( - _userManagerMock, - _httpContextAccessor, - role); - - var context = new AuthorizationHandlerContext(_requirements, claims, null); - - await _sut.HandleAsync(context); - Assert.Equal(shouldSucceed, context.HasSucceeded); - } - } -} From a4c3011ee8326c3140abbe6de872f7438c58fd1e Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Thu, 9 Feb 2023 08:50:44 +0100 Subject: [PATCH 14/69] Update Jellyfin.Data/DayOfWeekHelper.cs --- Jellyfin.Data/DayOfWeekHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Data/DayOfWeekHelper.cs b/Jellyfin.Data/DayOfWeekHelper.cs index d1ce8185f2..82abfb8313 100644 --- a/Jellyfin.Data/DayOfWeekHelper.cs +++ b/Jellyfin.Data/DayOfWeekHelper.cs @@ -23,7 +23,7 @@ namespace Jellyfin.Data return dynamicDayOfWeek switch { DynamicDayOfWeek.Everyday => true, - DynamicDayOfWeek.Weekday => dayOfWeek is > DayOfWeek.Sunday and <= DayOfWeek.Friday, + DynamicDayOfWeek.Weekday => dayOfWeek is >= DayOfWeek.Monday and <= DayOfWeek.Friday, DynamicDayOfWeek.Weekend => dayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday, _ => (DayOfWeek)dynamicDayOfWeek == dayOfWeek }; From f984f31896d9f5b34b488efb845d73f901fc9a80 Mon Sep 17 00:00:00 2001 From: cvium Date: Thu, 9 Feb 2023 08:53:59 +0100 Subject: [PATCH 15/69] admins shouldn't be able to circumvent remote access policies --- .../DefaultAuthorizationHandler.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs index 7489e2a35c..0f3c69abc8 100644 --- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs @@ -38,13 +38,6 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy /// protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement) { - // Admins can do everything - if (context.User.GetIsApiKey() || context.User.IsInRole(UserRoles.Administrator)) - { - context.Succeed(requirement); - return Task.CompletedTask; - } - var userId = context.User.GetUserId(); // This likely only happens during the wizard, so skip the default checks and let any other handlers do it if (userId.Equals(default)) @@ -62,6 +55,13 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy return Task.CompletedTask; } + // Admins can do everything + if (context.User.GetIsApiKey() || context.User.IsInRole(UserRoles.Administrator)) + { + context.Succeed(requirement); + return Task.CompletedTask; + } + // It's not great to have this check, but parental schedule must usually be honored except in a few rare cases if (requirement.ValidateParentalSchedule && !user.IsParentalScheduleAllowed()) { From f4a7583c46e25c5146953bef144f91f0c4c4519e Mon Sep 17 00:00:00 2001 From: cvium Date: Thu, 9 Feb 2023 12:51:20 +0100 Subject: [PATCH 16/69] fix empty user id check for api keys --- .../DefaultAuthorizationHandler.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs index 0f3c69abc8..2d9ce0631f 100644 --- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs @@ -38,9 +38,10 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy /// protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement) { + var isApiKey = context.User.GetIsApiKey(); var userId = context.User.GetUserId(); // This likely only happens during the wizard, so skip the default checks and let any other handlers do it - if (userId.Equals(default)) + if (!isApiKey && userId.Equals(default)) { return Task.CompletedTask; } @@ -56,7 +57,7 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy } // Admins can do everything - if (context.User.GetIsApiKey() || context.User.IsInRole(UserRoles.Administrator)) + if (isApiKey || context.User.IsInRole(UserRoles.Administrator)) { context.Succeed(requirement); return Task.CompletedTask; From 956c89dc2f5d6c3b8a3f362564b174fe303a9401 Mon Sep 17 00:00:00 2001 From: cvium Date: Thu, 9 Feb 2023 13:15:58 +0100 Subject: [PATCH 17/69] fix default policy --- Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index e2dcaf5f5c..9eaed703af 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -63,7 +63,7 @@ namespace Jellyfin.Server.Extensions { options.DefaultPolicy = new AuthorizationPolicyBuilder() .AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication) - .RequireAuthenticatedUser() + .AddRequirements(new DefaultAuthorizationRequirement()) .Build(); options.AddPolicy(Policies.Download, new UserPermissionRequirement(PermissionKind.EnableContentDownloading)); From b5d56679656539b1b01f995c9cffffad81aa2a21 Mon Sep 17 00:00:00 2001 From: cvium Date: Thu, 9 Feb 2023 14:40:50 +0100 Subject: [PATCH 18/69] remove a hardcoded DefaultAuthorization --- MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs b/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs index ac3df1d5d6..450ee2a337 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs @@ -11,7 +11,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Api /// The TMDb API controller. /// [ApiController] - [Authorize(Policy = "DefaultAuthorization")] + [Authorize] [Route("[controller]")] [Produces(MediaTypeNames.Application.Json)] public class TmdbController : ControllerBase From cba9657aec3c24c6724b00671019bdc212c41a90 Mon Sep 17 00:00:00 2001 From: cvium Date: Thu, 9 Feb 2023 14:56:53 +0100 Subject: [PATCH 19/69] fix openapi auth --- .../SecurityRequirementsOperationFilter.cs | 75 +++++++++++-------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs b/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs index 4af670e9a0..fb9f6d0a6e 100644 --- a/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs +++ b/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs @@ -18,11 +18,17 @@ namespace Jellyfin.Server.Filters { var requiredScopes = new List(); + var requiresAuth = false; // Add all method scopes. foreach (var attribute in context.MethodInfo.GetCustomAttributes(true)) { - if (attribute is AuthorizeAttribute authorizeAttribute - && authorizeAttribute.Policy is not null + if (attribute is not AuthorizeAttribute authorizeAttribute) + { + continue; + } + + requiresAuth = true; + if (authorizeAttribute.Policy is not null && !requiredScopes.Contains(authorizeAttribute.Policy, StringComparer.Ordinal)) { requiredScopes.Add(authorizeAttribute.Policy); @@ -35,8 +41,13 @@ namespace Jellyfin.Server.Filters { foreach (var attribute in controllerAttributes) { - if (attribute is AuthorizeAttribute authorizeAttribute - && authorizeAttribute.Policy is not null + if (attribute is not AuthorizeAttribute authorizeAttribute) + { + continue; + } + + requiresAuth = true; + if (authorizeAttribute.Policy is not null && !requiredScopes.Contains(authorizeAttribute.Policy, StringComparer.Ordinal)) { requiredScopes.Add(authorizeAttribute.Policy); @@ -44,35 +55,37 @@ namespace Jellyfin.Server.Filters } } - if (requiredScopes.Count != 0) + if (!requiresAuth) { - if (!operation.Responses.ContainsKey("401")) - { - operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" }); - } - - if (!operation.Responses.ContainsKey("403")) - { - operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" }); - } - - var scheme = new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = AuthenticationSchemes.CustomAuthentication - } - }; - - operation.Security = new List - { - new OpenApiSecurityRequirement - { - [scheme] = requiredScopes - } - }; + return; } + + if (!operation.Responses.ContainsKey("401")) + { + operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" }); + } + + if (!operation.Responses.ContainsKey("403")) + { + operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" }); + } + + var scheme = new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = AuthenticationSchemes.CustomAuthentication + } + }; + + operation.Security = new List + { + new OpenApiSecurityRequirement + { + [scheme] = requiredScopes + } + }; } } } From ac118e10f04de5489e6d0ac17c8ca16bdf6d0dc3 Mon Sep 17 00:00:00 2001 From: cvium Date: Thu, 9 Feb 2023 15:01:04 +0100 Subject: [PATCH 20/69] remove unnecessary init --- .../DefaultAuthorizationRequirement.cs | 2 +- .../Auth/FirstTimeSetupPolicy/FirstTimeSetupRequirement.cs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs index 0846e7515a..5ba1bc330d 100644 --- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs @@ -19,6 +19,6 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy /// /// Gets a value indicating whether to ignore parental schedule. /// - public bool ValidateParentalSchedule { get; init; } + public bool ValidateParentalSchedule { get; } } } diff --git a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupRequirement.cs b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupRequirement.cs index 8b7a94954e..6252a2feb8 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupRequirement.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupRequirement.cs @@ -12,9 +12,8 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy /// /// A value indicating whether to ignore parental schedule. /// A value indicating whether administrator role is required. - public FirstTimeSetupRequirement(bool validateParentalSchedule = false, bool requireAdmin = true) + public FirstTimeSetupRequirement(bool validateParentalSchedule = false, bool requireAdmin = true) : base(validateParentalSchedule) { - ValidateParentalSchedule = validateParentalSchedule; RequireAdmin = requireAdmin; } From c9aef96dba26d29df3db9e3a1ce243be23bea772 Mon Sep 17 00:00:00 2001 From: cvium Date: Thu, 9 Feb 2023 21:06:51 +0100 Subject: [PATCH 21/69] fix firsttimesetup --- .../Extensions/ApiServiceCollectionExtensions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 9eaed703af..968a8e58ce 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -67,9 +67,9 @@ namespace Jellyfin.Server.Extensions .Build(); options.AddPolicy(Policies.Download, new UserPermissionRequirement(PermissionKind.EnableContentDownloading)); - options.AddPolicy(Policies.FirstTimeSetupOrDefault, new FirstTimeSetupRequirement()); - options.AddPolicy(Policies.FirstTimeSetupOrElevated, new FirstTimeSetupRequirement(requireAdmin: true)); - options.AddPolicy(Policies.FirstTimeSetupOrIgnoreParentalControl, new FirstTimeSetupRequirement(validateParentalSchedule: false)); + options.AddPolicy(Policies.FirstTimeSetupOrDefault, new FirstTimeSetupRequirement(requireAdmin: false)); + options.AddPolicy(Policies.FirstTimeSetupOrElevated, new FirstTimeSetupRequirement()); + options.AddPolicy(Policies.FirstTimeSetupOrIgnoreParentalControl, new FirstTimeSetupRequirement(false, false)); options.AddPolicy(Policies.IgnoreParentalControl, new DefaultAuthorizationRequirement(validateParentalSchedule: false)); options.AddPolicy(Policies.SyncPlayHasAccess, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.HasAccess)); options.AddPolicy(Policies.SyncPlayCreateGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup)); From 724d2986a313823f5ded5c5745ab193cbd89e921 Mon Sep 17 00:00:00 2001 From: holow29 <67209066+holow29@users.noreply.github.com> Date: Mon, 6 Feb 2023 00:40:49 -0500 Subject: [PATCH 22/69] Change transcoderChannelLimit default to 8 Change transcoderChannelLimit default to 8 from 6 Switch to querying for encoder and added more cases to transcoderChannelLimit Refactor GetNumAudioChannelsParam --- .../MediaEncoding/EncodingHelper.cs | 100 +++++++----------- 1 file changed, 36 insertions(+), 64 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 9b5edabc06..98a6f47ba6 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2211,87 +2211,59 @@ namespace MediaBrowser.Controller.MediaEncoding var request = state.BaseRequest; - var inputChannels = audioStream.Channels; - - if (inputChannels <= 0) - { - inputChannels = null; - } - var codec = outputAudioCodec ?? string.Empty; - int? transcoderChannelLimit; - if (codec.IndexOf("wma", StringComparison.OrdinalIgnoreCase) != -1) + int? resultChannels = state.GetRequestedAudioChannels(codec); + + var inputChannels = audioStream.Channels; + + if (inputChannels > 0) { - // wmav2 currently only supports two channel output - transcoderChannelLimit = 2; - } - else if (codec.IndexOf("mp3", StringComparison.OrdinalIgnoreCase) != -1) - { - // libmp3lame currently only supports two channel output - transcoderChannelLimit = 2; - } - else if (codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1) - { - // aac is able to handle 8ch(7.1 layout) - transcoderChannelLimit = 8; - } - else - { - // If we don't have any media info then limit it to 6 to prevent encoding errors due to asking for too many channels - transcoderChannelLimit = 6; + resultChannels = inputChannels < resultChannels ? inputChannels : resultChannels ?? inputChannels; } var isTranscodingAudio = !IsCopyCodec(codec); - int? resultChannels = state.GetRequestedAudioChannels(codec); if (isTranscodingAudio) { - resultChannels = GetMinValue(request.TranscodingMaxAudioChannels, resultChannels); - } + // Set max transcoding channels for encoders that can't handle more than a set amount of channels + // AAC, FLAC, ALAC, libopus, libvorbis encoders all support at least 8 channels + int transcoderChannelLimit = GetAudioEncoder(state) switch + { + string audioEncoder when audioEncoder.Equals("wmav2", StringComparison.OrdinalIgnoreCase) + || audioEncoder.Equals("libmp3lame", StringComparison.OrdinalIgnoreCase) => 2, + string audioEncoder when audioEncoder.Equals("libfdk_aac", StringComparison.OrdinalIgnoreCase) + || audioEncoder.Equals("aac_at", StringComparison.OrdinalIgnoreCase) + || audioEncoder.Equals("ac3", StringComparison.OrdinalIgnoreCase) + || audioEncoder.Equals("eac3", StringComparison.OrdinalIgnoreCase) + || audioEncoder.Equals("dts", StringComparison.OrdinalIgnoreCase) + || audioEncoder.Equals("mlp", StringComparison.OrdinalIgnoreCase) + || audioEncoder.Equals("truehd", StringComparison.OrdinalIgnoreCase) => 6, + // Set default max transcoding channels to 8 to prevent encoding errors due to asking for too many channels + _ => 8, + }; - if (inputChannels.HasValue) - { - resultChannels = resultChannels.HasValue - ? Math.Min(resultChannels.Value, inputChannels.Value) - : inputChannels.Value; - } + // Set resultChannels to minimum between resultChannels, TranscodingMaxAudioChannels, transcoderChannelLimit - if (isTranscodingAudio && transcoderChannelLimit.HasValue) - { - resultChannels = resultChannels.HasValue - ? Math.Min(resultChannels.Value, transcoderChannelLimit.Value) - : transcoderChannelLimit.Value; - } + resultChannels = transcoderChannelLimit < resultChannels ? transcoderChannelLimit : resultChannels ?? transcoderChannelLimit; - // Avoid transcoding to audio channels other than 1ch, 2ch, 6ch (5.1 layout) and 8ch (7.1 layout). - // https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices - if (isTranscodingAudio - && state.TranscodingType != TranscodingJobType.Progressive - && resultChannels.HasValue - && ((resultChannels.Value > 2 && resultChannels.Value < 6) || resultChannels.Value == 7)) - { - resultChannels = 2; + if (request.TranscodingMaxAudioChannels < resultChannels) + { + resultChannels = request.TranscodingMaxAudioChannels; + } + + // Avoid transcoding to audio channels other than 1ch, 2ch, 6ch (5.1 layout) and 8ch (7.1 layout). + // https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices + if (state.TranscodingType != TranscodingJobType.Progressive + && ((resultChannels > 2 && resultChannels < 6) || resultChannels == 7)) + { + resultChannels = 2; + } } return resultChannels; } - private int? GetMinValue(int? val1, int? val2) - { - if (!val1.HasValue) - { - return val2; - } - - if (!val2.HasValue) - { - return val1; - } - - return Math.Min(val1.Value, val2.Value); - } - /// /// Enforces the resolution limit. /// From cb61a57e828f1e35b32cdb020a57bb4df35a5b3a Mon Sep 17 00:00:00 2001 From: Jpuc1143 <80900349+Jpuc1143@users.noreply.github.com> Date: Thu, 9 Feb 2023 20:45:40 -0300 Subject: [PATCH 23/69] Reduced number of calls to GetPreference() Co-authored-by: Cody Robibero --- MediaBrowser.Controller/Entities/BaseItem.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 0cd9720549..3d683052fe 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1607,7 +1607,8 @@ namespace MediaBrowser.Controller.Entities return false; } - if (user.GetPreference(PreferenceKind.AllowedTags).Any() && !user.GetPreference(PreferenceKind.AllowedTags).Any(i => Tags.Contains(i, StringComparison.OrdinalIgnoreCase))) + var allowedTagsPreference = user.GetPreference(PreferenceKind.AllowedTags); + if (allowedTagsPreference.Any() && !allowedTagsPreference.Any(i => Tags.Contains(i, StringComparison.OrdinalIgnoreCase))) { return false; } From 4323f69cd4176ed59fc3982a3d35b73a2c261d2a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Feb 2023 20:10:08 +0000 Subject: [PATCH 24/69] chore(deps): update github/codeql-action digest to 17573ee --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ef9ab45a17..41d59e2435 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@8775e868027fa230df8586bdf502bbd9b618a477 # v2 + uses: github/codeql-action/init@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@8775e868027fa230df8586bdf502bbd9b618a477 # v2 + uses: github/codeql-action/autobuild@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@8775e868027fa230df8586bdf502bbd9b618a477 # v2 + uses: github/codeql-action/analyze@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2 From 32eccc139c4b35aef04a29cefb11c6130726f617 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Sat, 11 Feb 2023 07:46:52 -0700 Subject: [PATCH 25/69] LiveTV fixes --- .../Data/SqliteItemRepository.cs | 3 +++ .../LiveTv/Listings/XmlTvListingsProvider.cs | 23 ++++++++++--------- .../Listings/XmlTvListingsProviderTests.cs | 19 +++++++++++++++ .../LiveTv/Listings/XmlTv/emptycategory.xml | 6 +++++ 4 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index bc703fe90d..9aa7eea848 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -5440,6 +5440,9 @@ AND Type = @InternalPersonType)"); list.AddRange(inheritedTags.Select(i => (6, i))); + // Remove all invalid values. + list.RemoveAll(i => string.IsNullOrEmpty(i.Item2)); + return list; } diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs index e874990da1..066afb956b 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -137,32 +137,33 @@ namespace Emby.Server.Implementations.LiveTv.Listings private static ProgramInfo GetProgramInfo(XmlTvProgram program, ListingsProviderInfo info) { - string episodeTitle = program.Episode?.Title; + string episodeTitle = program.Episode.Title; + var programCategories = program.Categories.Where(c => !string.IsNullOrWhiteSpace(c)).ToList(); var programInfo = new ProgramInfo { ChannelId = program.ChannelId, EndDate = program.EndDate.UtcDateTime, - EpisodeNumber = program.Episode?.Episode, + EpisodeNumber = program.Episode.Episode, EpisodeTitle = episodeTitle, - Genres = program.Categories, + Genres = programCategories, StartDate = program.StartDate.UtcDateTime, Name = program.Title, Overview = program.Description, ProductionYear = program.CopyrightDate?.Year, - SeasonNumber = program.Episode?.Series, - IsSeries = program.Episode is not null, + SeasonNumber = program.Episode.Series, + IsSeries = program.Episode.Series is not null, IsRepeat = program.IsPreviouslyShown && !program.IsNew, IsPremiere = program.Premiere is not null, - IsKids = program.Categories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), - IsMovie = program.Categories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), - IsNews = program.Categories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), - IsSports = program.Categories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), + IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), + IsMovie = programCategories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), + IsNews = programCategories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), + IsSports = programCategories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), ImageUrl = string.IsNullOrEmpty(program.Icon?.Source) ? null : program.Icon.Source, HasImage = !string.IsNullOrEmpty(program.Icon?.Source), OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value, CommunityRating = program.StarRating, - SeriesId = program.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture) + SeriesId = program.Episode.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture) }; if (string.IsNullOrWhiteSpace(program.ProgramId)) @@ -243,7 +244,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings { Id = c.Id, Name = c.DisplayName, - ImageUrl = string.IsNullOrEmpty(c.Icon.Source) ? null : c.Icon.Source, + ImageUrl = string.IsNullOrEmpty(c.Icon?.Source) ? null : c.Icon.Source, Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number }).ToList(); } diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs index 82ce8fc4ec..ab1896c0d7 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs @@ -67,4 +67,23 @@ public class XmlTvListingsProviderTests Assert.Equal("https://domain.tld/image.png", program.ImageUrl); Assert.Equal("3297", program.ChannelId); } + + [Theory] + [InlineData("Test Data/LiveTv/Listings/XmlTv/emptycategory.xml")] + [InlineData("https://example.com/emptycategory.xml")] + public async Task GetProgramsAsync_EmptyCategories_Success(string path) + { + var info = new ListingsProviderInfo() + { + Path = path + }; + + var startDate = new DateTime(2022, 11, 4); + var programs = await _xmlTvListingsProvider.GetProgramsAsync(info, "3297", startDate, startDate.AddDays(1), CancellationToken.None); + var programsList = programs.ToList(); + Assert.Single(programsList); + var program = programsList[0]; + Assert.DoesNotContain(program.Genres, g => string.Equals(g, string.Empty, StringComparison.Ordinal)); + Assert.Equal("3297", program.ChannelId); + } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml new file mode 100644 index 0000000000..dd4aa89774 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml @@ -0,0 +1,6 @@ + + + + sports + + From 6fb2fac6e4b81dce2a7fc59d3b8163f08c117f4f Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Sun, 12 Feb 2023 18:54:55 +0100 Subject: [PATCH 26/69] Always run code analyzers for tests projects (#9304) --- .../Jellyfin.Extensions.csproj | 2 +- .../Jellyfin.MediaEncoding.Hls.csproj | 2 +- .../Jellyfin.MediaEncoding.Keyframes.csproj | 2 +- tests/Directory.Build.props | 23 +++++++++++++++++++ .../Jellyfin.Api.Tests.csproj | 17 -------------- .../Jellyfin.Common.Tests.csproj | 17 -------------- .../Jellyfin.Controller.Tests.csproj | 17 -------------- .../Jellyfin.Dlna.Tests.csproj | 17 -------------- .../Jellyfin.Extensions.Tests.csproj | 17 -------------- .../Jellyfin.MediaEncoding.Hls.Tests.csproj | 16 ------------- ...lyfin.MediaEncoding.Keyframes.Tests.csproj | 18 --------------- .../Jellyfin.MediaEncoding.Tests.csproj | 17 -------------- .../Jellyfin.Model.Tests.csproj | 17 -------------- .../Jellyfin.Naming.Tests.csproj | 17 -------------- .../Jellyfin.Networking.Tests.csproj | 17 -------------- .../Jellyfin.Providers.Tests.csproj | 17 -------------- ...llyfin.Server.Implementations.Tests.csproj | 18 --------------- .../Jellyfin.Server.Integration.Tests.csproj | 16 ------------- .../Jellyfin.Server.Tests.csproj | 17 -------------- .../Jellyfin.XbmcMetadata.Tests.csproj | 17 -------------- 20 files changed, 26 insertions(+), 275 deletions(-) create mode 100644 tests/Directory.Build.props diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index 15261bb65e..4f80aa9416 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -33,7 +33,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj index f489d6fd0a..3f4f55ee41 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj +++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj @@ -6,7 +6,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj index 3801a1cc33..71572bcf6a 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj +++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj @@ -10,7 +10,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props new file mode 100644 index 0000000000..de8fc1bb8b --- /dev/null +++ b/tests/Directory.Build.props @@ -0,0 +1,23 @@ + + + + + + + net7.0 + false + $(MSBuildThisFileDirectory)/jellyfin-tests.ruleset + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index 6202f83dcf..0150189108 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -5,12 +5,6 @@ {A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} - - net7.0 - false - ../jellyfin-tests.ruleset - - @@ -27,17 +21,6 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj index 699c12217c..8fef7fde05 100644 --- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj +++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj @@ -5,12 +5,6 @@ {DF194677-DFD3-42AF-9F75-D44D5A416478} - - net7.0 - false - ../jellyfin-tests.ruleset - - @@ -22,17 +16,6 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj index 1e729a46a4..54d93b48cf 100644 --- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj +++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj @@ -5,12 +5,6 @@ {462584F7-5023-4019-9EAC-B98CA458C0A0} - - net7.0 - false - ../jellyfin-tests.ruleset - - @@ -22,17 +16,6 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj index 2be5da2c2c..69677ce424 100644 --- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj +++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj @@ -1,11 +1,5 @@ - - net7.0 - false - ../jellyfin-tests.ruleset - - @@ -17,17 +11,6 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - diff --git a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj index dbbb61cc45..0364898298 100644 --- a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj +++ b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj @@ -1,11 +1,5 @@ - - net7.0 - false - ../jellyfin-tests.ruleset - - @@ -20,17 +14,6 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - diff --git a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj index 10c1418731..eab003715c 100644 --- a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj @@ -1,11 +1,5 @@ - - net7.0 - false - ../jellyfin-tests.ruleset - - @@ -19,16 +13,6 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - - - diff --git a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj index 4910a041a3..894bec6aa5 100644 --- a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj @@ -1,12 +1,5 @@ - - net7.0 - false - ../jellyfin-tests.ruleset - Jellyfin.MediaEncoding.Keyframes - - @@ -20,17 +13,6 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj index 077466b6e6..6b703e7416 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj @@ -5,12 +5,6 @@ {28464062-0939-4AA7-9F7B-24DDDA61A7C0} - - net7.0 - false - ../jellyfin-tests.ruleset - - PreserveNewest @@ -31,17 +25,6 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj index cffd7bc0bb..8345b610e5 100644 --- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj +++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj @@ -1,11 +1,5 @@ - - net7.0 - false - ../jellyfin-tests.ruleset - - @@ -24,17 +18,6 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj index c5e93f0bbe..112dd780e3 100644 --- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj +++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj @@ -5,12 +5,6 @@ {3998657B-1CCC-49DD-A19F-275DC8495F57} - - net7.0 - false - ../jellyfin-tests.ruleset - - @@ -26,15 +20,4 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj index e245699269..4b4bdd2a51 100644 --- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj +++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj @@ -5,12 +5,6 @@ {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6} - - net7.0 - false - ../jellyfin-tests.ruleset - - @@ -23,17 +17,6 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj index 27151c8479..c12f0cd685 100644 --- a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj +++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj @@ -1,11 +1,5 @@ - - net7.0 - false - ../jellyfin-tests.ruleset - - PreserveNewest @@ -26,17 +20,6 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index 2150520e5f..9b6cb40b05 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -5,13 +5,6 @@ {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} - - net7.0 - false - ../jellyfin-tests.ruleset - Jellyfin.Server.Implementations.Tests - - PreserveNewest @@ -32,17 +25,6 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj index 26b2cd2395..a5296d8c93 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj +++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj @@ -1,9 +1,4 @@ - - net7.0 - false - ../jellyfin-tests.ruleset - @@ -29,17 +24,6 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj index d47f70cffa..5fea805ae1 100644 --- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj +++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj @@ -1,11 +1,5 @@ - - net7.0 - false - ../jellyfin-tests.ruleset - - @@ -22,17 +16,6 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj index fb7864cd13..9fe0744de1 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj +++ b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj @@ -1,11 +1,5 @@ - - net7.0 - false - ../jellyfin-tests.ruleset - - PreserveNewest @@ -23,17 +17,6 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - From 318f11e79331e4786c44734ce496eb6485201c2b Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Sun, 12 Feb 2023 19:25:54 +0100 Subject: [PATCH 27/69] Fix error in XmlTvListingsProviderTests (#9302) --- .../LiveTv/Listings/XmlTvListingsProviderTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs index ab1896c0d7..92b4178fdb 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs @@ -83,7 +83,7 @@ public class XmlTvListingsProviderTests var programsList = programs.ToList(); Assert.Single(programsList); var program = programsList[0]; - Assert.DoesNotContain(program.Genres, g => string.Equals(g, string.Empty, StringComparison.Ordinal)); + Assert.DoesNotContain(program.Genres, g => string.IsNullOrEmpty(g)); Assert.Equal("3297", program.ChannelId); } } From a5e2ae4979ece439ade037ba2c88a4003a7e8f68 Mon Sep 17 00:00:00 2001 From: cvium Date: Sun, 12 Feb 2023 23:01:30 +0100 Subject: [PATCH 28/69] fix merge conflict --- .../DefaultAuthorizationHandler.cs | 5 +++++ .../Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs | 6 ++++++ .../Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs | 5 +++++ .../Auth/UserPermissionPolicy/UserPermissionHandler.cs | 6 ++++++ 4 files changed, 22 insertions(+) diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs index 2d9ce0631f..b1d97e4a1d 100644 --- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs @@ -49,6 +49,11 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy var isInLocalNetwork = _httpContextAccessor.HttpContext is not null && _networkManager.IsInLocalNetwork(_httpContextAccessor.HttpContext.GetNormalizedRemoteIp()); var user = _userManager.GetUserById(userId); + if (user is null) + { + throw new ResourceNotFoundException(); + } + // User cannot access remotely and user is remote if (!isInLocalNetwork && !user.HasPermission(PermissionKind.EnableRemoteAccess)) { diff --git a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs index 302e052a7c..28ba258503 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; @@ -50,6 +51,11 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy } var user = _userManager.GetUserById(context.User.GetUserId()); + if (user is null) + { + throw new ResourceNotFoundException(); + } + if (user.IsParentalScheduleAllowed()) { context.Succeed(requirement); diff --git a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs index 5c1029b383..75ec9fcec6 100644 --- a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs +++ b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Extensions; using Jellyfin.Data.Enums; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.SyncPlay; using Microsoft.AspNetCore.Authorization; @@ -33,6 +34,10 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy { var userId = context.User.GetUserId(); var user = _userManager.GetUserById(userId); + if (user is null) + { + throw new ResourceNotFoundException(); + } if (requirement.RequiredAccess == SyncPlayAccessRequirementType.HasAccess) { diff --git a/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs index c3de7be328..ba2b1b657e 100644 --- a/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs +++ b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Auth.DownloadPolicy; using Jellyfin.Api.Extensions; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; @@ -26,6 +27,11 @@ namespace Jellyfin.Api.Auth.UserPermissionPolicy protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserPermissionRequirement requirement) { var user = _userManager.GetUserById(context.User.GetUserId()); + if (user is null) + { + throw new ResourceNotFoundException(); + } + if (user.HasPermission(requirement.RequiredPermission)) { context.Succeed(requirement); From 2d2b0a528c3d060754d05cd4e6f58e0e624f6301 Mon Sep 17 00:00:00 2001 From: Joe Rogers <1337joe@gmail.com> Date: Sun, 12 Feb 2023 21:59:58 -0500 Subject: [PATCH 29/69] Add missing checks for item locked state in metadata updates --- MediaBrowser.Providers/Manager/MetadataService.cs | 6 ++++++ MediaBrowser.Providers/MediaInfo/AudioFileProber.cs | 5 ++++- MediaBrowser.Providers/Music/AlbumMetadataService.cs | 6 ++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index ffae772008..ec99e7ffa4 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -334,6 +334,12 @@ namespace MediaBrowser.Providers.Manager updateType |= UpdateCumulativeRunTimeTicks(item, children); updateType |= UpdateDateLastMediaAdded(item, children); + // don't update user-changeable metadata for locked items + if (item.IsLocked) + { + return updateType; + } + if (EnableUpdatingPremiereDateFromChildren) { updateType |= UpdatePremiereDate(item, children); diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 74210b1f22..19b594c1c8 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -105,7 +105,10 @@ namespace MediaBrowser.Providers.MediaInfo audio.RunTimeTicks = mediaInfo.RunTimeTicks; audio.Size = mediaInfo.Size; - FetchDataFromTags(audio); + if (!audio.IsLocked) + { + FetchDataFromTags(audio); + } _itemRepo.SaveMediaStreams(audio.Id, mediaInfo.MediaStreams, cancellationToken); } diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs index 58cd23aa34..3476e70009 100644 --- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs +++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs @@ -54,6 +54,12 @@ namespace MediaBrowser.Providers.Music { var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType); + // don't update user-changeable metadata for locked items + if (item.IsLocked) + { + return updateType; + } + if (isFullRefresh || currentUpdateType > ItemUpdateType.None) { if (!item.LockedFields.Contains(MetadataField.Name)) From 4ce30989e0a0b70b2b425f6dd269306d8270716c Mon Sep 17 00:00:00 2001 From: Joe Rogers <1337joe@gmail.com> Date: Sun, 12 Feb 2023 23:14:43 -0500 Subject: [PATCH 30/69] Make update type for RunTimeTicks consistent with other file attributes --- MediaBrowser.Providers/Manager/MetadataService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index ec99e7ffa4..9f287766af 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -381,7 +381,7 @@ namespace MediaBrowser.Providers.Manager if (!folder.RunTimeTicks.HasValue || folder.RunTimeTicks.Value != ticks) { folder.RunTimeTicks = ticks; - return ItemUpdateType.MetadataEdit; + return ItemUpdateType.MetadataImport; } } From b7418d6e9e08af44edfc30971cd7b1b7871554c7 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Mon, 13 Feb 2023 15:42:04 +0100 Subject: [PATCH 31/69] Add permission for collection management --- .../Auth/UserPermissionPolicy/UserPermissionHandler.cs | 3 +-- .../Auth/UserPermissionPolicy/UserPermissionRequirement.cs | 2 +- Jellyfin.Api/Constants/Policies.cs | 5 +++++ Jellyfin.Api/Controllers/CollectionController.cs | 3 ++- Jellyfin.Data/Entities/User.cs | 1 + Jellyfin.Data/Enums/PermissionKind.cs | 7 ++++++- Jellyfin.Server.Implementations/Users/UserManager.cs | 2 ++ .../Extensions/ApiServiceCollectionExtensions.cs | 2 +- Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs | 1 + MediaBrowser.Controller/Entities/Movies/BoxSet.cs | 2 +- MediaBrowser.Model/Users/UserPolicy.cs | 7 +++++++ 11 files changed, 28 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs index ba2b1b657e..e72bec46fd 100644 --- a/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs +++ b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; -using Jellyfin.Api.Auth.DownloadPolicy; using Jellyfin.Api.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; @@ -8,7 +7,7 @@ using Microsoft.AspNetCore.Authorization; namespace Jellyfin.Api.Auth.UserPermissionPolicy { /// - /// Download authorization handler. + /// User permission authorization handler. /// public class UserPermissionHandler : AuthorizationHandler { diff --git a/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs index 195a611992..4694556eb7 100644 --- a/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs +++ b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs @@ -1,7 +1,7 @@ using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Data.Enums; -namespace Jellyfin.Api.Auth.DownloadPolicy +namespace Jellyfin.Api.Auth.UserPermissionPolicy { /// /// The user permission requirement. diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs index adc95e57b2..1ef38ca072 100644 --- a/Jellyfin.Api/Constants/Policies.cs +++ b/Jellyfin.Api/Constants/Policies.cs @@ -69,4 +69,9 @@ public static class Policies /// Policy name for accessing a SyncPlay group. /// public const string SyncPlayIsInGroup = "SyncPlayIsInGroup"; + + /// + /// Policy name for accessing collection management. + /// + public const string CollectionManagement = "CollectionManagement"; } diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs index f9f9be7ce4..2db04afb80 100644 --- a/Jellyfin.Api/Controllers/CollectionController.cs +++ b/Jellyfin.Api/Controllers/CollectionController.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; +using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using MediaBrowser.Controller.Collections; @@ -16,7 +17,7 @@ namespace Jellyfin.Api.Controllers; /// The collection controller. /// [Route("Collections")] -[Authorize] +[Authorize(Policy = Policies.CollectionManagement)] public class CollectionController : BaseJellyfinApiController { private readonly ICollectionManager _collectionManager; diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs index 4ce581749f..606e1b5427 100644 --- a/Jellyfin.Data/Entities/User.cs +++ b/Jellyfin.Data/Entities/User.cs @@ -508,6 +508,7 @@ namespace Jellyfin.Data.Entities Permissions.Add(new Permission(PermissionKind.EnableVideoPlaybackTranscoding, true)); Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false)); Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false)); + Permissions.Add(new Permission(PermissionKind.EnableCollectionManagement, false)); } /// diff --git a/Jellyfin.Data/Enums/PermissionKind.cs b/Jellyfin.Data/Enums/PermissionKind.cs index 7d52008747..40280b95ef 100644 --- a/Jellyfin.Data/Enums/PermissionKind.cs +++ b/Jellyfin.Data/Enums/PermissionKind.cs @@ -108,6 +108,11 @@ namespace Jellyfin.Data.Enums /// /// Whether the server should force transcoding on remote connections for the user. /// - ForceRemoteSourceTranscoding = 20 + ForceRemoteSourceTranscoding = 20, + + /// + /// Whether the user can create, modify and delete collections. + /// + EnableCollectionManagement = 21 } } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index f9679510d7..92384986af 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -369,6 +369,7 @@ namespace Jellyfin.Server.Implementations.Users EnablePlaybackRemuxing = user.HasPermission(PermissionKind.EnablePlaybackRemuxing), ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding), EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing), + EnableCollectionManagement = user.HasPermission(PermissionKind.EnableCollectionManagement), AccessSchedules = user.AccessSchedules.ToArray(), BlockedTags = user.GetPreference(PreferenceKind.BlockedTags), AllowedTags = user.GetPreference(PreferenceKind.AllowedTags), @@ -685,6 +686,7 @@ namespace Jellyfin.Server.Implementations.Users user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders); user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers); user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing); + user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement); user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding); user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing); diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 968a8e58ce..dffcfbba87 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -10,7 +10,6 @@ using Emby.Server.Implementations; using Jellyfin.Api.Auth; using Jellyfin.Api.Auth.AnonymousLanAccessPolicy; using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; -using Jellyfin.Api.Auth.DownloadPolicy; using Jellyfin.Api.Auth.FirstTimeSetupPolicy; using Jellyfin.Api.Auth.SyncPlayAccessPolicy; using Jellyfin.Api.Auth.UserPermissionPolicy; @@ -75,6 +74,7 @@ namespace Jellyfin.Server.Extensions options.AddPolicy(Policies.SyncPlayCreateGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup)); options.AddPolicy(Policies.SyncPlayJoinGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup)); options.AddPolicy(Policies.SyncPlayIsInGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup)); + options.AddPolicy(Policies.CollectionManagement, new UserPermissionRequirement(PermissionKind.EnableCollectionManagement)); options.AddPolicy(Policies.AnonymousLanAccessPolicy, new AnonymousLanAccessRequirement()); options.AddPolicy( Policies.RequiresElevation, diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index ea2f033027..9bf1e6b808 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -163,6 +163,7 @@ namespace Jellyfin.Server.Migrations.Routines user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing); user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding); user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing); + user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement); foreach (var policyAccessSchedule in policy.AccessSchedules) { diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index 882abc9272..66210cb6c4 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -104,7 +104,7 @@ namespace MediaBrowser.Controller.Entities.Movies public override bool IsAuthorizedToDelete(User user, List allCollectionFolders) { - return true; + return user.HasPermission(PermissionKind.IsAdministrator) || user.HasPermission(PermissionKind.EnableCollectionManagement); } public override bool IsSaveLocalMetadataEnabled() diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 1619dac5ad..cc7d57eb0a 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -13,6 +13,7 @@ namespace MediaBrowser.Model.Users public UserPolicy() { IsHidden = true; + EnableCollectionManagement = false; EnableContentDeletion = false; EnableContentDeletionFromFolders = Array.Empty(); @@ -73,6 +74,12 @@ namespace MediaBrowser.Model.Users /// true if this instance is hidden; otherwise, false. public bool IsHidden { get; set; } + /// + /// Gets or sets a value indicating whether this instance can manage collections. + /// + /// true if this instance is hidden; otherwise, false. + public bool EnableCollectionManagement { get; set; } + /// /// Gets or sets a value indicating whether this instance is disabled. /// From eeb0f7af6c1d3f422b66128e5a91830a3682e331 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Mon, 13 Feb 2023 15:38:18 +0100 Subject: [PATCH 32/69] Add permissions for LiveTV access and management --- Jellyfin.Api/Constants/Policies.cs | 10 ++ Jellyfin.Api/Controllers/LiveTvController.cs | 112 +++++++----------- .../ApiServiceCollectionExtensions.cs | 3 +- 3 files changed, 52 insertions(+), 73 deletions(-) diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs index 1ef38ca072..53841b0c44 100644 --- a/Jellyfin.Api/Constants/Policies.cs +++ b/Jellyfin.Api/Constants/Policies.cs @@ -74,4 +74,14 @@ public static class Policies /// Policy name for accessing collection management. /// public const string CollectionManagement = "CollectionManagement"; + + /// + /// Policy name for accessing LiveTV. + /// + public const string LiveTvAccess = "LiveTvAccess"; + + /// + /// Policy name for managing LiveTV. + /// + public const string LiveTvManagement = "LiveTvManagement"; } diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 3425c85890..318ed5c673 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -10,20 +10,19 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; +using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LiveTvDtos; using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -94,7 +93,7 @@ public class LiveTvController : BaseJellyfinApiController /// [HttpGet("Info")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] public ActionResult GetLiveTvInfo() { return _liveTvManager.GetLiveTvInfo(CancellationToken.None); @@ -130,7 +129,7 @@ public class LiveTvController : BaseJellyfinApiController /// [HttpGet("Channels")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] public ActionResult> GetLiveTvChannels( [FromQuery] ChannelType? type, [FromQuery] Guid? userId, @@ -209,7 +208,7 @@ public class LiveTvController : BaseJellyfinApiController /// An containing the live tv channel. [HttpGet("Channels/{channelId}")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] public ActionResult GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId) { var user = userId is null || userId.Value.Equals(default) @@ -250,7 +249,7 @@ public class LiveTvController : BaseJellyfinApiController /// An containing the live tv recordings. [HttpGet("Recordings")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] public ActionResult> GetRecordings( [FromQuery] string? channelId, [FromQuery] Guid? userId, @@ -321,7 +320,7 @@ public class LiveTvController : BaseJellyfinApiController /// An containing the live tv recordings. [HttpGet("Recordings/Series")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] [Obsolete("This endpoint is obsolete.")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] @@ -364,7 +363,7 @@ public class LiveTvController : BaseJellyfinApiController /// An containing the recording groups. [HttpGet("Recordings/Groups")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] [Obsolete("This endpoint is obsolete.")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] public ActionResult> GetRecordingGroups([FromQuery] Guid? userId) @@ -380,7 +379,7 @@ public class LiveTvController : BaseJellyfinApiController /// An containing the recording folders. [HttpGet("Recordings/Folders")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] public ActionResult> GetRecordingFolders([FromQuery] Guid? userId) { var user = userId is null || userId.Value.Equals(default) @@ -402,7 +401,7 @@ public class LiveTvController : BaseJellyfinApiController /// An containing the live tv recording. [HttpGet("Recordings/{recordingId}")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] public ActionResult GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId) { var user = userId is null || userId.Value.Equals(default) @@ -424,10 +423,9 @@ public class LiveTvController : BaseJellyfinApiController /// A . [HttpPost("Tuners/{tunerId}/Reset")] [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize] + [Authorize(Policy = Policies.LiveTvManagement)] public async Task ResetTuner([FromRoute, Required] string tunerId) { - await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -442,7 +440,7 @@ public class LiveTvController : BaseJellyfinApiController /// [HttpGet("Timers/{timerId}")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] public async Task> GetTimer([FromRoute, Required] string timerId) { return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false); @@ -458,7 +456,7 @@ public class LiveTvController : BaseJellyfinApiController /// [HttpGet("Timers/Defaults")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] public async Task> GetDefaultTimer([FromQuery] string? programId) { return string.IsNullOrEmpty(programId) @@ -478,7 +476,7 @@ public class LiveTvController : BaseJellyfinApiController /// [HttpGet("Timers")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] public async Task>> GetTimers( [FromQuery] string? channelId, [FromQuery] string? seriesTimerId, @@ -532,7 +530,7 @@ public class LiveTvController : BaseJellyfinApiController /// [HttpGet("Programs")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] public async Task>> GetLiveTvPrograms( [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds, [FromQuery] Guid? userId, @@ -615,7 +613,7 @@ public class LiveTvController : BaseJellyfinApiController /// [HttpPost("Programs")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] public async Task>> GetPrograms([FromBody] GetProgramsDto body) { var user = body.UserId.Equals(default) ? null : _userManager.GetUserById(body.UserId); @@ -681,7 +679,7 @@ public class LiveTvController : BaseJellyfinApiController /// Recommended epgs returned. /// A containing the queryresult of recommended epgs. [HttpGet("Programs/Recommended")] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetRecommendedPrograms( [FromQuery] Guid? userId, @@ -733,7 +731,7 @@ public class LiveTvController : BaseJellyfinApiController /// Program returned. /// An containing the livetv program. [HttpGet("Programs/{programId}")] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetProgram( [FromRoute, Required] string programId, @@ -754,13 +752,11 @@ public class LiveTvController : BaseJellyfinApiController /// Item not found. /// A on success, or a if item not found. [HttpDelete("Recordings/{recordingId}")] - [Authorize] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task DeleteRecording([FromRoute, Required] Guid recordingId) + public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId) { - await AssertUserCanManageLiveTv().ConfigureAwait(false); - var item = _libraryManager.GetItemById(recordingId); if (item is null) { @@ -782,11 +778,10 @@ public class LiveTvController : BaseJellyfinApiController /// Timer deleted. /// A . [HttpDelete("Timers/{timerId}")] - [Authorize] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task CancelTimer([FromRoute, Required] string timerId) { - await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false); return NoContent(); } @@ -799,12 +794,11 @@ public class LiveTvController : BaseJellyfinApiController /// Timer updated. /// A . [HttpPost("Timers/{timerId}")] - [Authorize] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status204NoContent)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")] public async Task UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo) { - await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -816,11 +810,10 @@ public class LiveTvController : BaseJellyfinApiController /// Timer created. /// A . [HttpPost("Timers")] - [Authorize] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task CreateTimer([FromBody] TimerInfoDto timerInfo) { - await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -833,7 +826,7 @@ public class LiveTvController : BaseJellyfinApiController /// Series timer not found. /// A on success, or a if timer not found. [HttpGet("SeriesTimers/{timerId}")] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetSeriesTimer([FromRoute, Required] string timerId) @@ -855,7 +848,7 @@ public class LiveTvController : BaseJellyfinApiController /// Timers returned. /// An of live tv series timers. [HttpGet("SeriesTimers")] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetSeriesTimers([FromQuery] string? sortBy, [FromQuery] SortOrder? sortOrder) { @@ -875,11 +868,10 @@ public class LiveTvController : BaseJellyfinApiController /// Timer cancelled. /// A . [HttpDelete("SeriesTimers/{timerId}")] - [Authorize] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task CancelSeriesTimer([FromRoute, Required] string timerId) { - await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false); return NoContent(); } @@ -892,12 +884,11 @@ public class LiveTvController : BaseJellyfinApiController /// Series timer updated. /// A . [HttpPost("SeriesTimers/{timerId}")] - [Authorize] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status204NoContent)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")] public async Task UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo) { - await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -909,11 +900,10 @@ public class LiveTvController : BaseJellyfinApiController /// Series timer info created. /// A . [HttpPost("SeriesTimers")] - [Authorize] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo) { - await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -924,7 +914,7 @@ public class LiveTvController : BaseJellyfinApiController /// Group id. /// A . [HttpGet("Recordings/Groups/{groupId}")] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status404NotFound)] [Obsolete("This endpoint is obsolete.")] public ActionResult GetRecordingGroup([FromRoute, Required] Guid groupId) @@ -938,7 +928,7 @@ public class LiveTvController : BaseJellyfinApiController /// Guid info returned. /// An containing the guide info. [HttpGet("GuideInfo")] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetGuideInfo() { @@ -952,7 +942,7 @@ public class LiveTvController : BaseJellyfinApiController /// Created tuner host returned. /// A containing the created tuner host. [HttpPost("TunerHosts")] - [Authorize] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo) { @@ -966,7 +956,7 @@ public class LiveTvController : BaseJellyfinApiController /// Tuner host deleted. /// A . [HttpDelete("TunerHosts")] - [Authorize] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult DeleteTunerHost([FromQuery] string? id) { @@ -982,7 +972,7 @@ public class LiveTvController : BaseJellyfinApiController /// Default listings provider info returned. /// An containing the default listings provider info. [HttpGet("ListingProviders/Default")] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetDefaultListingProvider() { @@ -999,7 +989,7 @@ public class LiveTvController : BaseJellyfinApiController /// Created listings provider returned. /// A containing the created listings provider. [HttpPost("ListingProviders")] - [Authorize] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")] public async Task> AddListingProvider( @@ -1025,7 +1015,7 @@ public class LiveTvController : BaseJellyfinApiController /// Listing provider deleted. /// A . [HttpDelete("ListingProviders")] - [Authorize] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult DeleteListingProvider([FromQuery] string? id) { @@ -1043,7 +1033,7 @@ public class LiveTvController : BaseJellyfinApiController /// Available lineups returned. /// A containing the available lineups. [HttpGet("ListingProviders/Lineups")] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetLineups( [FromQuery] string? id, @@ -1060,7 +1050,7 @@ public class LiveTvController : BaseJellyfinApiController /// Available countries returned. /// A containing the available countries. [HttpGet("ListingProviders/SchedulesDirect/Countries")] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesFile(MediaTypeNames.Application.Json)] public async Task GetSchedulesDirectCountries() @@ -1081,7 +1071,7 @@ public class LiveTvController : BaseJellyfinApiController /// Channel mapping options returned. /// An containing the channel mapping options. [HttpGet("ChannelMappingOptions")] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetChannelMappingOptions([FromQuery] string? providerId) { @@ -1119,7 +1109,7 @@ public class LiveTvController : BaseJellyfinApiController /// Created channel mapping returned. /// An containing the created channel mapping. [HttpPost("ChannelMappings")] - [Authorize] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto) { @@ -1132,7 +1122,7 @@ public class LiveTvController : BaseJellyfinApiController /// Tuner host types returned. /// An containing the tuner host types. [HttpGet("TunerHosts/Types")] - [Authorize] + [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetTunerHostTypes() { @@ -1147,7 +1137,7 @@ public class LiveTvController : BaseJellyfinApiController /// An containing the tuners. [HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")] [HttpGet("Tuners/Discover")] - [Authorize] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> DiscoverTuners([FromQuery] bool newDevicesOnly = false) { @@ -1207,26 +1197,4 @@ public class LiveTvController : BaseJellyfinApiController var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream()); return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container)); } - - private async Task AssertUserCanManageLiveTv() - { - var user = _userManager.GetUserById(User.GetUserId()) ?? throw new ResourceNotFoundException(); - var session = await _sessionManager.LogSessionActivity( - User.GetClient(), - User.GetVersion(), - User.GetDeviceId(), - User.GetDevice(), - HttpContext.GetNormalizedRemoteIp().ToString(), - user).ConfigureAwait(false); - - if (session.UserId.Equals(default)) - { - throw new SecurityException("Anonymous live tv management is not allowed."); - } - - if (!user.HasPermission(PermissionKind.EnableLiveTvManagement)) - { - throw new SecurityException("The current user does not have permission to manage live tv."); - } - } } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index dffcfbba87..61957b4eae 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -75,7 +75,8 @@ namespace Jellyfin.Server.Extensions options.AddPolicy(Policies.SyncPlayJoinGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup)); options.AddPolicy(Policies.SyncPlayIsInGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup)); options.AddPolicy(Policies.CollectionManagement, new UserPermissionRequirement(PermissionKind.EnableCollectionManagement)); - options.AddPolicy(Policies.AnonymousLanAccessPolicy, new AnonymousLanAccessRequirement()); + options.AddPolicy(Policies.LiveTvAccess, new UserPermissionRequirement(PermissionKind.EnableLiveTvAccess)); + options.AddPolicy(Policies.LiveTvManagement, new UserPermissionRequirement(PermissionKind.EnableLiveTvManagement)); options.AddPolicy( Policies.RequiresElevation, policy => policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication) From b8ed1f81cda7222078ed245c5f46562fc7822758 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 14 Feb 2023 19:04:18 +0100 Subject: [PATCH 33/69] Add back LocalAccessOrRequiresElevationPolicy --- .../LocalAccessOrRequiresElevationHandler.cs | 52 +++++++++++++++++++ ...calAccessOrRequiresElevationRequirement.cs | 11 ++++ .../ApiServiceCollectionExtensions.cs | 9 ++-- 3 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs create mode 100644 Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs new file mode 100644 index 0000000000..0b0877d068 --- /dev/null +++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs @@ -0,0 +1,52 @@ +using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy +{ + /// + /// Local access or require elevated privileges handler. + /// + public class LocalAccessOrRequiresElevationHandler : AuthorizationHandler + { + private readonly INetworkManager _networkManager; + private readonly IHttpContextAccessor _httpContextAccessor; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + public LocalAccessOrRequiresElevationHandler( + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor) + { + _networkManager = networkManager; + _httpContextAccessor = httpContextAccessor; + } + + /// + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessOrRequiresElevationRequirement requirement) + { + var ip = _httpContextAccessor.HttpContext?.GetNormalizedRemoteIp(); + + // Loopback will be on LAN, so we can accept null. + if (ip is null || _networkManager.IsInLocalNetwork(ip)) + { + context.Succeed(requirement); + } + + if (context.User.IsInRole(UserRoles.Administrator)) + { + context.Succeed(requirement); + } + + context.Fail(); + + return Task.CompletedTask; + } + } +} diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs new file mode 100644 index 0000000000..f633c69d8f --- /dev/null +++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy +{ + /// + /// The local access or elevated privileges authorization requirement. + /// + public class LocalAccessOrRequiresElevationRequirement : IAuthorizationRequirement + { + } +} diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 61957b4eae..9867c9e47a 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ using Jellyfin.Api.Auth; using Jellyfin.Api.Auth.AnonymousLanAccessPolicy; using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Auth.FirstTimeSetupPolicy; +using Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy; using Jellyfin.Api.Auth.SyncPlayAccessPolicy; using Jellyfin.Api.Auth.UserPermissionPolicy; using Jellyfin.Api.Constants; @@ -65,18 +66,20 @@ namespace Jellyfin.Server.Extensions .AddRequirements(new DefaultAuthorizationRequirement()) .Build(); + options.AddPolicy(Policies.AnonymousLanAccessPolicy, new AnonymousLanAccessRequirement()); + options.AddPolicy(Policies.CollectionManagement, new UserPermissionRequirement(PermissionKind.EnableCollectionManagement)); options.AddPolicy(Policies.Download, new UserPermissionRequirement(PermissionKind.EnableContentDownloading)); options.AddPolicy(Policies.FirstTimeSetupOrDefault, new FirstTimeSetupRequirement(requireAdmin: false)); options.AddPolicy(Policies.FirstTimeSetupOrElevated, new FirstTimeSetupRequirement()); options.AddPolicy(Policies.FirstTimeSetupOrIgnoreParentalControl, new FirstTimeSetupRequirement(false, false)); options.AddPolicy(Policies.IgnoreParentalControl, new DefaultAuthorizationRequirement(validateParentalSchedule: false)); + options.AddPolicy(Policies.LiveTvAccess, new UserPermissionRequirement(PermissionKind.EnableLiveTvAccess)); + options.AddPolicy(Policies.LiveTvManagement, new UserPermissionRequirement(PermissionKind.EnableLiveTvManagement)); + options.AddPolicy(Policies.LocalAccessOrRequiresElevation, new LocalAccessOrRequiresElevationRequirement()); options.AddPolicy(Policies.SyncPlayHasAccess, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.HasAccess)); options.AddPolicy(Policies.SyncPlayCreateGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup)); options.AddPolicy(Policies.SyncPlayJoinGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup)); options.AddPolicy(Policies.SyncPlayIsInGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup)); - options.AddPolicy(Policies.CollectionManagement, new UserPermissionRequirement(PermissionKind.EnableCollectionManagement)); - options.AddPolicy(Policies.LiveTvAccess, new UserPermissionRequirement(PermissionKind.EnableLiveTvAccess)); - options.AddPolicy(Policies.LiveTvManagement, new UserPermissionRequirement(PermissionKind.EnableLiveTvManagement)); options.AddPolicy( Policies.RequiresElevation, policy => policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication) From 36b7157589c07097df891738934ec0f351be69cb Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Tue, 14 Feb 2023 20:08:52 +0100 Subject: [PATCH 34/69] Fix #9300 (#9312) --- MediaBrowser.Common/Net/IPNetAddress.cs | 5 +++++ tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs | 1 + 2 files changed, 6 insertions(+) diff --git a/MediaBrowser.Common/Net/IPNetAddress.cs b/MediaBrowser.Common/Net/IPNetAddress.cs index ac3396a9f1..de72d978ec 100644 --- a/MediaBrowser.Common/Net/IPNetAddress.cs +++ b/MediaBrowser.Common/Net/IPNetAddress.cs @@ -167,6 +167,11 @@ namespace MediaBrowser.Common.Net address = address.MapToIPv4(); } + if (address.AddressFamily != AddressFamily) + { + return false; + } + var (altAddress, altPrefix) = NetworkAddressOf(address, PrefixLength); return NetworkAddress.Address.Equals(altAddress) && NetworkAddress.PrefixLength >= altPrefix; } diff --git a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs index 61f9132528..df2a2ca708 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs @@ -45,6 +45,7 @@ namespace Jellyfin.Networking.Tests [InlineData("fd23:184f:2029:0::/56", "fd24:184f:2029:0:3139:7386:67d7:d517")] [InlineData("fd23:184f:2029:0::/56, !fd23:184f:2029:0:3139:7386:67d7:d500/120", "fd23:184f:2029:0:3139:7386:67d7:d517")] [InlineData("fd23:184f:2029:0::/56", "192.168.10.60")] + [InlineData("2001:abcd:abcd:6b40::0/60", "192.168.10.60")] public void InNetwork_False_Success(string network, string value) { var ip = IPAddress.Parse(value); From 92f6e19a25def0fc699ff9a2190d54383f46451b Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Tue, 14 Feb 2023 20:09:07 +0100 Subject: [PATCH 35/69] Enable nullable for more files (#9310) --- .../AppBase/BaseConfigurationManager.cs | 52 ++++++------------- .../ServerConfigurationManager.cs | 4 +- .../Images/BaseFolderImageProvider.cs | 2 - .../Images/FolderImageProvider.cs | 2 - .../Images/GenreImageProvider.cs | 2 - 5 files changed, 18 insertions(+), 44 deletions(-) diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index 985a127d50..a4deeddb78 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -33,15 +31,10 @@ namespace Emby.Server.Implementations.AppBase private ConfigurationStore[] _configurationStores = Array.Empty(); private IConfigurationFactory[] _configurationFactories = Array.Empty(); - /// - /// The _configuration loaded. - /// - private bool _configurationLoaded; - /// /// The _configuration. /// - private BaseApplicationConfiguration _configuration; + private BaseApplicationConfiguration? _configuration; /// /// Initializes a new instance of the class. @@ -63,17 +56,17 @@ namespace Emby.Server.Implementations.AppBase /// /// Occurs when [configuration updated]. /// - public event EventHandler ConfigurationUpdated; + public event EventHandler? ConfigurationUpdated; /// /// Occurs when [configuration updating]. /// - public event EventHandler NamedConfigurationUpdating; + public event EventHandler? NamedConfigurationUpdating; /// /// Occurs when [named configuration updated]. /// - public event EventHandler NamedConfigurationUpdated; + public event EventHandler? NamedConfigurationUpdated; /// /// Gets the type of the configuration. @@ -107,31 +100,25 @@ namespace Emby.Server.Implementations.AppBase { get { - if (_configurationLoaded) + if (_configuration is not null) { return _configuration; } lock (_configurationSyncLock) { - if (_configurationLoaded) + if (_configuration is not null) { return _configuration; } - _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer); - - _configurationLoaded = true; - - return _configuration; + return _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer); } } protected set { _configuration = value; - - _configurationLoaded = value is not null; } } @@ -183,7 +170,7 @@ namespace Emby.Server.Implementations.AppBase Logger.LogInformation("Saving system configuration"); var path = CommonApplicationPaths.SystemConfigurationFilePath; - Directory.CreateDirectory(Path.GetDirectoryName(path)); + Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory.")); lock (_configurationSyncLock) { @@ -323,25 +310,20 @@ namespace Emby.Server.Implementations.AppBase private object LoadConfiguration(string path, Type configurationType) { - if (!File.Exists(path)) - { - return Activator.CreateInstance(configurationType); - } - try { - return XmlSerializer.DeserializeFromFile(configurationType, path); + if (File.Exists(path)) + { + return XmlSerializer.DeserializeFromFile(configurationType, path); + } } - catch (IOException) - { - return Activator.CreateInstance(configurationType); - } - catch (Exception ex) + catch (Exception ex) when (ex is not IOException) { Logger.LogError(ex, "Error loading configuration file: {Path}", path); - - return Activator.CreateInstance(configurationType); } + + return Activator.CreateInstance(configurationType) + ?? throw new InvalidOperationException("Configuration type can't be Nullable."); } /// @@ -367,7 +349,7 @@ namespace Emby.Server.Implementations.AppBase _configurations.AddOrUpdate(key, configuration, (_, _) => configuration); var path = GetConfigurationFile(key); - Directory.CreateDirectory(Path.GetDirectoryName(path)); + Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory.")); lock (_configurationSyncLock) { diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs index ff5602f243..6b8b1a620f 100644 --- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Globalization; using System.IO; @@ -36,7 +34,7 @@ namespace Emby.Server.Implementations.Configuration /// /// Configuration updating event. /// - public event EventHandler> ConfigurationUpdating; + public event EventHandler>? ConfigurationUpdating; /// /// Gets the type of the configuration. diff --git a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs index 6fc7f1ac3a..84c21931c3 100644 --- a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/Emby.Server.Implementations/Images/FolderImageProvider.cs b/Emby.Server.Implementations/Images/FolderImageProvider.cs index 4376bd356c..90f7568a90 100644 --- a/Emby.Server.Implementations/Images/FolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/FolderImageProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using MediaBrowser.Common.Configuration; diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs index 968bf5fa33..c9b41f8193 100644 --- a/Emby.Server.Implementations/Images/GenreImageProvider.cs +++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System.Collections.Generic; From 2c0201e9219e6fcb4fff8d6c9f1811feffafb713 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Feb 2023 12:09:50 -0700 Subject: [PATCH 36/69] chore(deps): update dotnet monorepo (#9311) --- Directory.Packages.props | 22 +++++++++++----------- deployment/Dockerfile.centos.amd64 | 2 +- deployment/Dockerfile.fedora.amd64 | 2 +- deployment/Dockerfile.ubuntu.amd64 | 2 +- deployment/Dockerfile.ubuntu.arm64 | 2 +- deployment/Dockerfile.ubuntu.armhf | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 26f0692115..b72bb90709 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,28 +23,28 @@ - - + + - - - - + + + + - + - - + + - + @@ -77,7 +77,7 @@ - + diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64 index e02087a525..95b08eb052 100644 --- a/deployment/Dockerfile.centos.amd64 +++ b/deployment/Dockerfile.centos.amd64 @@ -13,7 +13,7 @@ RUN yum update -yq \ && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64 index 6962b6bc18..18fb7bebe3 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -12,7 +12,7 @@ RUN dnf update -yq \ && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64 index 96e3ca403b..e0555cd220 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -17,7 +17,7 @@ RUN apt-get update -yqq \ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64 index f1c5363999..ad5a0890b4 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf index eaea305d1e..2d8be18351 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet From 87b2bc5dc4b19f275488178fec25e56e23047fec Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 14 Feb 2023 20:22:07 +0100 Subject: [PATCH 37/69] Fix LocalAccessOrRequiresElevationHandler (#9315) --- .../LocalAccessOrRequiresElevationHandler.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs index 0b0877d068..6ed6fc90be 100644 --- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs +++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs @@ -37,14 +37,18 @@ namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy if (ip is null || _networkManager.IsInLocalNetwork(ip)) { context.Succeed(requirement); + + return Task.CompletedTask; } if (context.User.IsInRole(UserRoles.Administrator)) { context.Succeed(requirement); } - - context.Fail(); + else + { + context.Fail(); + } return Task.CompletedTask; } From 59920b4052d60b27b9434058df308c3f30f541c4 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 15 Feb 2023 18:05:49 +0100 Subject: [PATCH 38/69] Make exact match primary video --- Emby.Naming/Video/VideoListResolver.cs | 20 +++++++++---------- .../Video/MultiVersionTests.cs | 19 ++++++++++++++++++ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index 8048320400..01e383d1c0 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -106,6 +106,7 @@ namespace Emby.Naming.Video } // Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the above [if] + VideoInfo? primary = null; for (var i = 0; i < videos.Count; i++) { var video = videos[i]; @@ -118,25 +119,24 @@ namespace Emby.Naming.Video { return videos; } + + if (folderName.Equals(Path.GetFileNameWithoutExtension(video.Files[0].Path.AsSpan()), StringComparison.Ordinal)) + { + primary = video; + } } // The list is created and overwritten in the caller, so we are allowed to do in-place sorting videos.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal)); + primary ??= videos[0]; + videos.Remove(primary); var list = new List { - videos[0] + primary }; - var alternateVersionsLen = videos.Count - 1; - var alternateVersions = new VideoFileInfo[alternateVersionsLen]; - for (int i = 0; i < alternateVersionsLen; i++) - { - var video = videos[i + 1]; - alternateVersions[i] = video.Files[0]; - } - - list[0].AlternateVersions = alternateVersions; + list[0].AlternateVersions = videos.Select(x => x.Files[0]).ToArray(); list[0].Name = folderName.ToString(); return list; diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs index 287d881a83..02e6f6368c 100644 --- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs @@ -323,6 +323,25 @@ namespace Jellyfin.Naming.Tests.Video Assert.Single(result[0].AlternateVersions); } + [Fact] + public void TestMultiVersion12() + { + var files = new[] + { + @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv", + @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv" + }; + + var result = VideoListResolver.Resolve( + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), + _namingOptions).ToList(); + + Assert.Single(result); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv", result[0].Files[0].Path); + Assert.Single(result[0].AlternateVersions); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv", result[0].AlternateVersions[0].Path); + } + [Fact] public void Resolve_GivenFolderNameWithBracketsAndHyphens_GroupsBasedOnFolderName() { From c338aa7cb5c1ce2c011a419c251184d247ea1e5b Mon Sep 17 00:00:00 2001 From: MBR#0001 Date: Wed, 15 Feb 2023 21:07:36 +0100 Subject: [PATCH 39/69] Fix NRE in DisposeAsyncCore --- Emby.Server.Implementations/ApplicationHost.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 37a9e7715d..d104058cc1 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1184,10 +1184,13 @@ namespace Emby.Server.Implementations } } - // used for closing websockets - foreach (var session in _sessionManager.Sessions) + if (_sessionManager != null) { - await session.DisposeAsync().ConfigureAwait(false); + // used for closing websockets + foreach (var session in _sessionManager.Sessions) + { + await session.DisposeAsync().ConfigureAwait(false); + } } } } From cb85fc688f84de928e046cb6bd02629f04d68df6 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 15 Feb 2023 23:41:28 +0100 Subject: [PATCH 40/69] Enable nullable for more files --- .../ScheduledTasks/TaskManager.cs | 6 ++---- .../Session/SessionWebSocketListener.cs | 13 ++++++++----- .../Sorting/RuntimeComparer.cs | 5 +---- .../Sorting/SeriesSortNameComparer.cs | 7 ++----- .../Sorting/SortNameComparer.cs | 5 +---- .../Sorting/StartDateComparer.cs | 6 ++---- .../Sorting/StudioComparer.cs | 5 +---- Emby.Server.Implementations/TV/TVSeriesManager.cs | 12 +++++------- .../Session/ISessionController.cs | 2 -- MediaBrowser.Model/Tasks/ITaskManager.cs | 4 ++-- 10 files changed, 24 insertions(+), 41 deletions(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs index 63f0beb105..6dc20e66ba 100644 --- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs +++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -43,9 +41,9 @@ namespace Emby.Server.Implementations.ScheduledTasks ScheduledTasks = Array.Empty(); } - public event EventHandler> TaskExecuting; + public event EventHandler>? TaskExecuting; - public event EventHandler TaskCompleted; + public event EventHandler? TaskCompleted; /// /// Gets the list of Scheduled Tasks. diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index aebb559073..4e427b1a4b 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -58,7 +56,7 @@ namespace Emby.Server.Implementations.Session /// /// The KeepAlive cancellation token. /// - private CancellationTokenSource _keepAliveCancellationToken; + private CancellationTokenSource? _keepAliveCancellationToken; /// /// Initializes a new instance of the class. @@ -105,7 +103,7 @@ namespace Emby.Server.Implementations.Session } } - private async Task GetSession(HttpContext httpContext, string remoteEndpoint) + private async Task GetSession(HttpContext httpContext, string? remoteEndpoint) { if (!httpContext.User.Identity?.IsAuthenticated ?? false) { @@ -138,8 +136,13 @@ namespace Emby.Server.Implementations.Session /// /// The WebSocket. /// The event arguments. - private void OnWebSocketClosed(object sender, EventArgs e) + private void OnWebSocketClosed(object? sender, EventArgs e) { + if (sender is null) + { + return; + } + var webSocket = (IWebSocketConnection)sender; _logger.LogDebug("WebSocket {0} is closed.", webSocket); RemoveWebSocket(webSocket); diff --git a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs index 646bafbb54..753e58324c 100644 --- a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs +++ b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; @@ -24,10 +22,9 @@ namespace Emby.Server.Implementations.Sorting /// The x. /// The y. /// System.Int32. - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { ArgumentNullException.ThrowIfNull(x); - ArgumentNullException.ThrowIfNull(y); return (x.RunTimeTicks ?? 0).CompareTo(y.RunTimeTicks ?? 0); diff --git a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs index 0bd9600b98..5b6c64f63a 100644 --- a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs +++ b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -23,15 +21,14 @@ namespace Emby.Server.Implementations.Sorting /// The x. /// The y. /// System.Int32. - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase); } - private static string GetValue(BaseItem item) + private static string? GetValue(BaseItem? item) { var hasSeries = item as IHasSeries; - return hasSeries?.FindSeriesSortName(); } } diff --git a/Emby.Server.Implementations/Sorting/SortNameComparer.cs b/Emby.Server.Implementations/Sorting/SortNameComparer.cs index 628b9b3dda..19abafe192 100644 --- a/Emby.Server.Implementations/Sorting/SortNameComparer.cs +++ b/Emby.Server.Implementations/Sorting/SortNameComparer.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; @@ -24,10 +22,9 @@ namespace Emby.Server.Implementations.Sorting /// The x. /// The y. /// System.Int32. - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { ArgumentNullException.ThrowIfNull(x); - ArgumentNullException.ThrowIfNull(y); return string.Compare(x.SortName, y.SortName, StringComparison.OrdinalIgnoreCase); diff --git a/Emby.Server.Implementations/Sorting/StartDateComparer.cs b/Emby.Server.Implementations/Sorting/StartDateComparer.cs index c3df7c47e6..2759d20de8 100644 --- a/Emby.Server.Implementations/Sorting/StartDateComparer.cs +++ b/Emby.Server.Implementations/Sorting/StartDateComparer.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -24,7 +22,7 @@ namespace Emby.Server.Implementations.Sorting /// The x. /// The y. /// System.Int32. - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { return GetDate(x).CompareTo(GetDate(y)); } @@ -34,7 +32,7 @@ namespace Emby.Server.Implementations.Sorting /// /// The x. /// DateTime. - private static DateTime GetDate(BaseItem x) + private static DateTime GetDate(BaseItem? x) { if (x is LiveTvProgram hasStartDate) { diff --git a/Emby.Server.Implementations/Sorting/StudioComparer.cs b/Emby.Server.Implementations/Sorting/StudioComparer.cs index 457c062714..89d10f3d23 100644 --- a/Emby.Server.Implementations/Sorting/StudioComparer.cs +++ b/Emby.Server.Implementations/Sorting/StudioComparer.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -24,10 +22,9 @@ namespace Emby.Server.Implementations.Sorting /// The x. /// The y. /// System.Int32. - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { ArgumentNullException.ThrowIfNull(x); - ArgumentNullException.ThrowIfNull(y); return AlphanumericComparator.CompareValues(x.Studios.FirstOrDefault(), y.Studios.FirstOrDefault()); diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index 967f90b55f..f0e173f0b1 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -42,7 +40,7 @@ namespace Emby.Server.Implementations.TV throw new ArgumentException("User not found"); } - string presentationUniqueKey = null; + string? presentationUniqueKey = null; if (query.SeriesId.HasValue && !query.SeriesId.Value.Equals(default)) { if (_libraryManager.GetItemById(query.SeriesId.Value) is Series series) @@ -91,7 +89,7 @@ namespace Emby.Server.Implementations.TV throw new ArgumentException("User not found"); } - string presentationUniqueKey = null; + string? presentationUniqueKey = null; int? limit = null; if (request.SeriesId.HasValue && !request.SeriesId.Value.Equals(default)) { @@ -168,7 +166,7 @@ namespace Emby.Server.Implementations.TV return !anyFound && i.LastWatchedDate == DateTime.MinValue; }) .Select(i => i.GetEpisodeFunction()) - .Where(i => i is not null); + .Where(i => i is not null)!; } private static string GetUniqueSeriesKey(Episode episode) @@ -185,7 +183,7 @@ namespace Emby.Server.Implementations.TV /// Gets the next up. /// /// Task{Episode}. - private (DateTime LastWatchedDate, Func GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching) + private (DateTime LastWatchedDate, Func GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching) { var lastQuery = new InternalItemsQuery(user) { @@ -209,7 +207,7 @@ namespace Emby.Server.Implementations.TV var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast().FirstOrDefault(); - Episode GetEpisode() + Episode? GetEpisode() { var nextQuery = new InternalItemsQuery(user) { diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs index b38ee11462..c8b29aa1f3 100644 --- a/MediaBrowser.Controller/Session/ISessionController.cs +++ b/MediaBrowser.Controller/Session/ISessionController.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Model/Tasks/ITaskManager.cs b/MediaBrowser.Model/Tasks/ITaskManager.cs index 13bebc479e..5b55667e82 100644 --- a/MediaBrowser.Model/Tasks/ITaskManager.cs +++ b/MediaBrowser.Model/Tasks/ITaskManager.cs @@ -9,9 +9,9 @@ namespace MediaBrowser.Model.Tasks { public interface ITaskManager : IDisposable { - event EventHandler> TaskExecuting; + event EventHandler>? TaskExecuting; - event EventHandler TaskCompleted; + event EventHandler? TaskCompleted; /// /// Gets the list of Scheduled Tasks. From 785e8c4085d5a83fec30fd02f835d0aa1c885633 Mon Sep 17 00:00:00 2001 From: Ruben Kremer Date: Wed, 15 Feb 2023 21:41:19 +0000 Subject: [PATCH 41/69] Translated using Weblate (Dutch) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/ --- .../Localization/Core/nl.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index e03747cbec..081ba0cc7e 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -58,8 +58,8 @@ "NotificationOptionServerRestartRequired": "Server herstart nodig", "NotificationOptionTaskFailed": "Geplande taak mislukt", "NotificationOptionUserLockedOut": "Gebruiker is vergrendeld", - "NotificationOptionVideoPlayback": "Video gestart", - "NotificationOptionVideoPlaybackStopped": "Video gestopt", + "NotificationOptionVideoPlayback": "Afspelen van video gestart", + "NotificationOptionVideoPlaybackStopped": "Afspelen van video gestopt", "Photos": "Foto's", "Playlists": "Afspeellijsten", "Plugin": "Plug-in", @@ -95,26 +95,26 @@ "TaskDownloadMissingSubtitlesDescription": "Zoekt op het internet naar ontbrekende ondertiteling gebaseerd op metadataconfiguratie.", "TaskDownloadMissingSubtitles": "Ontbrekende ondertiteling downloaden", "TaskRefreshChannelsDescription": "Vernieuwt informatie van internet kanalen.", - "TaskRefreshChannels": "Vernieuw Kanalen", + "TaskRefreshChannels": "Vernieuw kanalen", "TaskCleanTranscodeDescription": "Verwijdert transcode bestanden ouder dan 1 dag.", "TaskCleanLogs": "Logboekmap opschonen", "TaskCleanTranscode": "Transcoderingsmap opschonen", "TaskUpdatePluginsDescription": "Downloadt en installeert updates van plug-ins waarvoor automatisch bijwerken is ingeschakeld.", "TaskUpdatePlugins": "Plug-ins bijwerken", - "TaskRefreshPeopleDescription": "Update metadata for acteurs en regisseurs in de media bibliotheek.", + "TaskRefreshPeopleDescription": "Update metadata voor acteurs en regisseurs in de media bibliotheek.", "TaskRefreshPeople": "Personen vernieuwen", "TaskCleanLogsDescription": "Verwijdert log bestanden ouder dan {0} dagen.", "TaskRefreshLibraryDescription": "Scant de mediabibliotheek op nieuwe bestanden en vernieuwt de metadata.", "TaskRefreshLibrary": "Mediabibliotheek scannen", - "TaskRefreshChapterImagesDescription": "Maakt thumbnails aan voor videos met hoofdstukken.", - "TaskRefreshChapterImages": "Hoofdstukafbeeldingen uitpakken", + "TaskRefreshChapterImagesDescription": "Maakt voorbeeldafbeedingen aan voor video's met hoofdstukken.", + "TaskRefreshChapterImages": "Hoofdstukafbeeldingen extraheren", "TaskCleanCacheDescription": "Verwijdert gecachte bestanden die het systeem niet langer nodig heeft.", "TaskCleanCache": "Cache-map opschonen", - "TasksChannelsCategory": "Internet Kanalen", + "TasksChannelsCategory": "Internetkanalen", "TasksApplicationCategory": "Toepassing", "TasksLibraryCategory": "Bibliotheek", "TasksMaintenanceCategory": "Onderhoud", - "TaskCleanActivityLogDescription": "Verwijdert activiteiten logs ouder dan de ingestelde tijd.", + "TaskCleanActivityLogDescription": "Verwijdert activiteiten logs ouder dan de ingestelde leeftijd.", "TaskCleanActivityLog": "Activiteitenlogboek legen", "Undefined": "Niet gedefinieerd", "Forced": "Geforceerd", From 13589ceb064fc9b12f252b1eb107ae5f1e63d723 Mon Sep 17 00:00:00 2001 From: ikoch Date: Wed, 15 Feb 2023 12:03:24 +0000 Subject: [PATCH 42/69] Translated using Weblate (Russian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ru/ --- Emby.Server.Implementations/Localization/Core/ru.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index 65cf29e807..855223381c 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -23,7 +23,7 @@ "HeaderFavoriteShows": "Избранные сериалы", "HeaderFavoriteSongs": "Избранные композиции", "HeaderLiveTV": "Эфир", - "HeaderNextUp": "Очередное", + "HeaderNextUp": "Следующий", "HeaderRecordingGroups": "Группы записей", "HomeVideos": "Домашние видео", "Inherit": "Наследуемое", @@ -70,7 +70,7 @@ "ScheduledTaskFailedWithName": "{0} - неудачна", "ScheduledTaskStartedWithName": "{0} - запущена", "ServerNameNeedsToBeRestarted": "Необходим перезапуск {0}", - "Shows": "Передачи", + "Shows": "Телешоу", "Songs": "Композиции", "StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.", "SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить", From bfb31a9bceb294f1c75e0c6530e917cc0a9bf108 Mon Sep 17 00:00:00 2001 From: stegl Date: Tue, 14 Feb 2023 22:38:50 +0000 Subject: [PATCH 43/69] Translated using Weblate (Slovenian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sl/ --- Emby.Server.Implementations/Localization/Core/sl-SI.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json index d845accac2..4c23f71efa 100644 --- a/Emby.Server.Implementations/Localization/Core/sl-SI.json +++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Optimiziraj bazo podatkov", "TaskKeyframeExtractor": "Ekstraktor ključnih sličic", "External": "Zunanji", - "TaskKeyframeExtractorDescription": "Iz video datoteke Izvleče ključne sličice, da ustvari bolj natančne sezname predvajanja HLS. Proces lahko traja dolgo časa." + "TaskKeyframeExtractorDescription": "Iz video datoteke Izvleče ključne sličice, da ustvari bolj natančne sezname predvajanja HLS. Proces lahko traja dolgo časa.", + "HearingImpaired": "Oslabljen sluh" } From 65f6c2e2fd7b3a7f308463e3bc096ba6e8bf65da Mon Sep 17 00:00:00 2001 From: Ruben Wealth Hu Date: Wed, 15 Feb 2023 13:06:43 +0000 Subject: [PATCH 44/69] Translated using Weblate (Indonesian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/id/ --- Emby.Server.Implementations/Localization/Core/id.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json index 695c0f4048..87ce07da31 100644 --- a/Emby.Server.Implementations/Localization/Core/id.json +++ b/Emby.Server.Implementations/Localization/Core/id.json @@ -82,7 +82,7 @@ "MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui", "MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui", "FailedLoginAttemptWithUserName": "Gagal melakukan login dari {0}", - "CameraImageUploadedFrom": "Gambar kamera baru telah diunggah dari {0}", + "CameraImageUploadedFrom": "Sebuah gambar kamera baru telah diunggah dari {0}", "DeviceOfflineWithName": "{0} telah terputus", "DeviceOnlineWithName": "{0} telah terhubung", "NotificationOptionVideoPlaybackStopped": "Pemutaran video berhenti", From 60f41b80f66cc866c018c58e4567726cb6dac90d Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Tue, 10 Jan 2023 17:02:23 +0100 Subject: [PATCH 45/69] Verify ContentType of uploaded images --- Jellyfin.Api/Controllers/ImageController.cs | 65 ++++++++++++++----- MediaBrowser.Model/Net/MimeTypes.cs | 5 ++ .../Controllers/ImageControllerTests.cs | 36 ++++++++++ 3 files changed, 91 insertions(+), 15 deletions(-) create mode 100644 tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index aecdf00dc9..3c5f18af55 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -91,6 +91,7 @@ public class ImageController : BaseJellyfinApiController [Authorize] [AcceptsImageFile] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] @@ -110,6 +111,11 @@ public class ImageController : BaseJellyfinApiController return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image."); } + if (!TryGetImageExtensionFromContentType(Request.ContentType, out string? extension)) + { + return BadRequest("Incorrect ContentType."); + } + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); await using (memoryStream.ConfigureAwait(false)) { @@ -121,7 +127,7 @@ public class ImageController : BaseJellyfinApiController await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); } - user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty))); + user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension)); await _providerManager .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) @@ -145,6 +151,7 @@ public class ImageController : BaseJellyfinApiController [Authorize] [AcceptsImageFile] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] @@ -164,6 +171,11 @@ public class ImageController : BaseJellyfinApiController return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image."); } + if (!TryGetImageExtensionFromContentType(Request.ContentType, out string? extension)) + { + return BadRequest("Incorrect ContentType."); + } + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); await using (memoryStream.ConfigureAwait(false)) { @@ -175,7 +187,7 @@ public class ImageController : BaseJellyfinApiController await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); } - user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty))); + user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension)); await _providerManager .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) @@ -342,6 +354,7 @@ public class ImageController : BaseJellyfinApiController [Authorize(Policy = Policies.RequiresElevation)] [AcceptsImageFile] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] public async Task SetItemImage( @@ -354,6 +367,11 @@ public class ImageController : BaseJellyfinApiController return NotFound(); } + if (!TryGetImageExtensionFromContentType(Request.ContentType, out _)) + { + return BadRequest("Incorrect ContentType."); + } + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); await using (memoryStream.ConfigureAwait(false)) { @@ -379,6 +397,7 @@ public class ImageController : BaseJellyfinApiController [Authorize(Policy = Policies.RequiresElevation)] [AcceptsImageFile] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] public async Task SetItemImageByIndex( @@ -392,6 +411,11 @@ public class ImageController : BaseJellyfinApiController return NotFound(); } + if (!TryGetImageExtensionFromContentType(Request.ContentType, out _)) + { + return BadRequest("Incorrect ContentType."); + } + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); await using (memoryStream.ConfigureAwait(false)) { @@ -1763,22 +1787,14 @@ public class ImageController : BaseJellyfinApiController [AcceptsImageFile] public async Task UploadCustomSplashscreen() { + if (!TryGetImageExtensionFromContentType(Request.ContentType, out var extension)) + { + return BadRequest("Incorrect ContentType."); + } + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); await using (memoryStream.ConfigureAwait(false)) { - var mimeType = MediaTypeHeaderValue.Parse(Request.ContentType).MediaType; - - if (!mimeType.HasValue) - { - return BadRequest("Error reading mimetype from uploaded image"); - } - - var extension = MimeTypes.ToExtension(mimeType.Value); - if (string.IsNullOrEmpty(extension)) - { - return BadRequest("Error converting mimetype to an image extension"); - } - var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension); var brandingOptions = _serverConfigurationManager.GetConfiguration("branding"); brandingOptions.SplashscreenLocation = filePath; @@ -2106,4 +2122,23 @@ public class ImageController : BaseJellyfinApiController return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain); } + + internal static bool TryGetImageExtensionFromContentType(string? contentType, [NotNullWhen(true)] out string? extension) + { + extension = null; + if (string.IsNullOrEmpty(contentType)) + { + return false; + } + + if (MediaTypeHeaderValue.TryParse(contentType, out var parsedValue) + && parsedValue.MediaType.HasValue + && MimeTypes.IsImage(parsedValue.MediaType.Value)) + { + extension = MimeTypes.ToExtension(parsedValue.MediaType.Value); + return extension is not null; + } + + return false; + } } diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs index 8157dc0c24..5a1871070d 100644 --- a/MediaBrowser.Model/Net/MimeTypes.cs +++ b/MediaBrowser.Model/Net/MimeTypes.cs @@ -117,7 +117,9 @@ namespace MediaBrowser.Model.Net // Type image { "image/jpeg", ".jpg" }, + { "image/tiff", ".tiff" }, { "image/x-png", ".png" }, + { "image/x-icon", ".ico" }, // Type text { "text/plain", ".txt" }, @@ -178,5 +180,8 @@ namespace MediaBrowser.Model.Net var extension = Model.MimeTypes.GetMimeTypeExtensions(mimeType).FirstOrDefault(); return string.IsNullOrEmpty(extension) ? null : "." + extension; } + + public static bool IsImage(ReadOnlySpan mimeType) + => mimeType.StartsWith("image/", StringComparison.OrdinalIgnoreCase); } } diff --git a/tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs new file mode 100644 index 0000000000..d6428fb2cd --- /dev/null +++ b/tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs @@ -0,0 +1,36 @@ +using System; +using Jellyfin.Api.Controllers; +using Xunit; + +namespace Jellyfin.Api.Tests.Controllers; + +public static class ImageControllerTests +{ + [Theory] + [InlineData("image/apng", ".apng")] + [InlineData("image/avif", ".avif")] + [InlineData("image/bmp", ".bmp")] + [InlineData("image/gif", ".gif")] + [InlineData("image/x-icon", ".ico")] + [InlineData("image/jpeg", ".jpg")] + [InlineData("image/png", ".png")] + [InlineData("image/png; charset=utf-8", ".png")] + [InlineData("image/svg+xml", ".svg")] + [InlineData("image/tiff", ".tiff")] + [InlineData("image/webp", ".webp")] + public static void TryGetImageExtensionFromContentType_Valid_True(string contentType, string extension) + { + Assert.True(ImageController.TryGetImageExtensionFromContentType(contentType, out var ex)); + Assert.Equal(extension, ex); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("text/html")] + public static void TryGetImageExtensionFromContentType_InValid_False(string contentType) + { + Assert.False(ImageController.TryGetImageExtensionFromContentType(contentType, out var ex)); + Assert.Null(ex); + } +} From a38cb3ade8f3dc50e1a5d968c6b6ac68306bc5bb Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Tue, 10 Jan 2023 17:15:21 +0100 Subject: [PATCH 46/69] Fix tests --- tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs b/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs index cbab455f01..371c3811ab 100644 --- a/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs +++ b/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs @@ -127,9 +127,10 @@ namespace Jellyfin.Model.Tests.Net [InlineData("image/jpeg", ".jpg")] [InlineData("image/png", ".png")] [InlineData("image/svg+xml", ".svg")] - [InlineData("image/tiff", ".tif")] + [InlineData("image/tiff", ".tiff")] [InlineData("image/vnd.microsoft.icon", ".ico")] [InlineData("image/webp", ".webp")] + [InlineData("image/x-icon", ".ico")] [InlineData("image/x-png", ".png")] [InlineData("text/css", ".css")] [InlineData("text/csv", ".csv")] From 5071973170577c5fb33640396912f3b9e05ac5b9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Feb 2023 05:44:56 -0700 Subject: [PATCH 47/69] chore(deps): update dependency prometheus-net to v8 (#9333) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b72bb90709..d5f762fe83 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -55,7 +55,7 @@ - + From 48263078b46aa4ef46c0fb6944665b2c317bf077 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Fri, 17 Feb 2023 15:00:06 +0100 Subject: [PATCH 48/69] Reduce string allocations by regex --- .../AudioBook/AudioBookFilePathParser.cs | 4 +-- Emby.Naming/AudioBook/AudioBookNameParser.cs | 2 +- Emby.Naming/Common/NamingOptions.cs | 32 ------------------- Emby.Naming/TV/EpisodePathParser.cs | 14 ++++---- Emby.Naming/Video/CleanDateTimeParser.cs | 2 +- Emby.Naming/Video/ExtraRuleResolver.cs | 2 +- Emby.Naming/Video/VideoListResolver.cs | 9 +++--- .../Library/Resolvers/Movies/MovieResolver.cs | 9 ++---- .../Users/UserManager.cs | 2 +- .../Encoder/EncoderValidator.cs | 6 ++-- .../Common/NamingOptionsTest.cs | 2 -- 11 files changed, 22 insertions(+), 62 deletions(-) diff --git a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs index 7b4429ab15..219599d569 100644 --- a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs +++ b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs @@ -40,7 +40,7 @@ namespace Emby.Naming.AudioBook var value = match.Groups["chapter"]; if (value.Success) { - if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) + if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) { result.ChapterNumber = intValue; } @@ -52,7 +52,7 @@ namespace Emby.Naming.AudioBook var value = match.Groups["part"]; if (value.Success) { - if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) + if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) { result.PartNumber = intValue; } diff --git a/Emby.Naming/AudioBook/AudioBookNameParser.cs b/Emby.Naming/AudioBook/AudioBookNameParser.cs index 97b34199e0..f49c3f0e75 100644 --- a/Emby.Naming/AudioBook/AudioBookNameParser.cs +++ b/Emby.Naming/AudioBook/AudioBookNameParser.cs @@ -47,7 +47,7 @@ namespace Emby.Naming.AudioBook var value = match.Groups["year"]; if (value.Success) { - if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) + if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) { result.Year = intValue; } diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 54f62a1570..c16a71e02f 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -453,16 +453,6 @@ namespace Emby.Naming.Common }, }; - EpisodeWithoutSeasonExpressions = new[] - { - @"[/\._ \-]()([0-9]+)(-[0-9]+)?" - }; - - EpisodeMultiPartExpressions = new[] - { - @"^[-_ex]+([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)" - }; - VideoExtraRules = new[] { new ExtraRule( @@ -797,16 +787,6 @@ namespace Emby.Naming.Common /// public EpisodeExpression[] EpisodeExpressions { get; set; } - /// - /// Gets or sets list of raw episode without season regular expressions strings. - /// - public string[] EpisodeWithoutSeasonExpressions { get; set; } - - /// - /// Gets or sets list of raw multi-part episodes regular expressions strings. - /// - public string[] EpisodeMultiPartExpressions { get; set; } - /// /// Gets or sets list of video file extensions. /// @@ -877,16 +857,6 @@ namespace Emby.Naming.Common /// public Regex[] CleanStringRegexes { get; private set; } = Array.Empty(); - /// - /// Gets list of episode without season regular expressions. - /// - public Regex[] EpisodeWithoutSeasonRegexes { get; private set; } = Array.Empty(); - - /// - /// Gets list of multi-part episode regular expressions. - /// - public Regex[] EpisodeMultiPartRegexes { get; private set; } = Array.Empty(); - /// /// Compiles raw regex strings into regexes. /// @@ -894,8 +864,6 @@ namespace Emby.Naming.Common { CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray(); CleanStringRegexes = CleanStrings.Select(Compile).ToArray(); - EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray(); - EpisodeMultiPartRegexes = EpisodeMultiPartExpressions.Select(Compile).ToArray(); } private Regex Compile(string exp) diff --git a/Emby.Naming/TV/EpisodePathParser.cs b/Emby.Naming/TV/EpisodePathParser.cs index d706be2802..8cd5a126e0 100644 --- a/Emby.Naming/TV/EpisodePathParser.cs +++ b/Emby.Naming/TV/EpisodePathParser.cs @@ -113,7 +113,7 @@ namespace Emby.Naming.TV if (expression.DateTimeFormats.Length > 0) { if (DateTime.TryParseExact( - match.Groups[0].Value, + match.Groups[0].ValueSpan, expression.DateTimeFormats, CultureInfo.InvariantCulture, DateTimeStyles.None, @@ -125,7 +125,7 @@ namespace Emby.Naming.TV result.Success = true; } } - else if (DateTime.TryParse(match.Groups[0].Value, out date)) + else if (DateTime.TryParse(match.Groups[0].ValueSpan, out date)) { result.Year = date.Year; result.Month = date.Month; @@ -138,12 +138,12 @@ namespace Emby.Naming.TV } else if (expression.IsNamed) { - if (int.TryParse(match.Groups["seasonnumber"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(match.Groups["seasonnumber"].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num)) { result.SeasonNumber = num; } - if (int.TryParse(match.Groups["epnumber"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) + if (int.TryParse(match.Groups["epnumber"].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) { result.EpisodeNumber = num; } @@ -158,7 +158,7 @@ namespace Emby.Naming.TV if (nextIndex >= name.Length || !"0123456789iIpP".Contains(name[nextIndex], StringComparison.Ordinal)) { - if (int.TryParse(endingNumberGroup.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) + if (int.TryParse(endingNumberGroup.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) { result.EndingEpisodeNumber = num; } @@ -170,12 +170,12 @@ namespace Emby.Naming.TV } else { - if (int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(match.Groups[1].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num)) { result.SeasonNumber = num; } - if (int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) + if (int.TryParse(match.Groups[2].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) { result.EpisodeNumber = num; } diff --git a/Emby.Naming/Video/CleanDateTimeParser.cs b/Emby.Naming/Video/CleanDateTimeParser.cs index 0ee633dcc6..9a6c6e978b 100644 --- a/Emby.Naming/Video/CleanDateTimeParser.cs +++ b/Emby.Naming/Video/CleanDateTimeParser.cs @@ -43,7 +43,7 @@ namespace Emby.Naming.Video && match.Groups.Count == 5 && match.Groups[1].Success && match.Groups[2].Success - && int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year)) + && int.TryParse(match.Groups[2].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year)) { result = new CleanDateTimeResult(match.Groups[1].Value.TrimEnd(), year); return true; diff --git a/Emby.Naming/Video/ExtraRuleResolver.cs b/Emby.Naming/Video/ExtraRuleResolver.cs index 21d0da3642..3219472eff 100644 --- a/Emby.Naming/Video/ExtraRuleResolver.cs +++ b/Emby.Naming/Video/ExtraRuleResolver.cs @@ -56,7 +56,7 @@ namespace Emby.Naming.Video } else if (rule.RuleType == ExtraRuleType.Regex) { - var filename = Path.GetFileName(path); + var filename = Path.GetFileName(path.AsSpan()); var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled); diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index 01e383d1c0..8247c374d0 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -176,16 +176,15 @@ namespace Emby.Naming.Video } // There are no span overloads for regex unfortunately - var tmpTestFilename = testFilename.ToString(); - if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName)) + if (CleanStringParser.TryClean(testFilename.ToString(), namingOptions.CleanStringRegexes, out var cleanName)) { - tmpTestFilename = cleanName.Trim(); + testFilename = cleanName.AsSpan().Trim(); } // The CleanStringParser should have removed common keywords etc. - return string.IsNullOrEmpty(tmpTestFilename) + return testFilename.IsEmpty || testFilename[0] == '-' - || Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled); + || Regex.IsMatch(testFilename, @"^\[([^]]*)\]", RegexOptions.Compiled); } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 1522cd3aef..ef4fa1fd2d 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -313,13 +313,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return result; } - private static bool IsIgnored(string filename) - { - // Ignore samples - Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled); - - return m.Success; - } + private static bool IsIgnored(ReadOnlySpan filename) + => Regex.IsMatch(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static bool ContainsFile(IReadOnlyList result, FileSystemMetadata file) { diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 92384986af..c4756433e0 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -740,7 +740,7 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)", nameof(name)); } - private static bool IsValidUsername(string name) + private static bool IsValidUsername(ReadOnlySpan name) { // This is some regex that matches only on unicode "word" characters, as well as -, _ and @ // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 9e6134b524..540d50bf15 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -277,7 +277,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (match.Success) { - if (Version.TryParse(match.Groups[1].Value, out var result)) + if (Version.TryParse(match.Groups[1].ValueSpan, out var result)) { return result; } @@ -327,8 +327,8 @@ namespace MediaBrowser.MediaEncoding.Encoder RegexOptions.Multiline)) { var version = new Version( - int.Parse(match.Groups["major"].Value, CultureInfo.InvariantCulture), - int.Parse(match.Groups["minor"].Value, CultureInfo.InvariantCulture)); + int.Parse(match.Groups["major"].ValueSpan, CultureInfo.InvariantCulture), + int.Parse(match.Groups["minor"].ValueSpan, CultureInfo.InvariantCulture)); map.Add(match.Groups["name"].Value, version); } diff --git a/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs b/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs index 58aaed023a..c496632482 100644 --- a/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs +++ b/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs @@ -12,8 +12,6 @@ namespace Jellyfin.Naming.Tests.Common Assert.NotEmpty(options.CleanDateTimeRegexes); Assert.NotEmpty(options.CleanStringRegexes); - Assert.NotEmpty(options.EpisodeWithoutSeasonRegexes); - Assert.NotEmpty(options.EpisodeMultiPartRegexes); } [Fact] From 3bec70302b69556533196cda53c6b03ecd356a0e Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Fri, 17 Feb 2023 20:47:07 +0100 Subject: [PATCH 49/69] Fix use after dispose --- .../Subtitles/SubtitleManager.cs | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index 12570f0a7e..d89fb814d8 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -193,43 +193,43 @@ namespace MediaBrowser.Providers.Subtitles await stream.CopyToAsync(memoryStream).ConfigureAwait(false); memoryStream.Position = 0; } - } - var savePaths = new List(); - var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant(); + var savePaths = new List(); + var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant(); - if (response.IsForced) - { - saveFileName += ".forced"; - } - - saveFileName += "." + response.Format.ToLowerInvariant(); - - if (saveInMediaFolder) - { - var mediaFolderPath = Path.GetFullPath(Path.Combine(video.ContainingFolderPath, saveFileName)); - // TODO: Add some error handling to the API user: return BadRequest("Could not save subtitle, bad path."); - if (mediaFolderPath.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal)) + if (response.IsForced) { - savePaths.Add(mediaFolderPath); + saveFileName += ".forced"; } - } - var internalPath = Path.GetFullPath(Path.Combine(video.GetInternalMetadataPath(), saveFileName)); + saveFileName += "." + response.Format.ToLowerInvariant(); - // TODO: Add some error to the user: return BadRequest("Could not save subtitle, bad path."); - if (internalPath.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal)) - { - savePaths.Add(internalPath); - } + if (saveInMediaFolder) + { + var mediaFolderPath = Path.GetFullPath(Path.Combine(video.ContainingFolderPath, saveFileName)); + // TODO: Add some error handling to the API user: return BadRequest("Could not save subtitle, bad path."); + if (mediaFolderPath.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal)) + { + savePaths.Add(mediaFolderPath); + } + } - if (savePaths.Count > 0) - { - await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false); - } - else - { - _logger.LogError("An uploaded subtitle could not be saved because the resulting paths were invalid."); + var internalPath = Path.GetFullPath(Path.Combine(video.GetInternalMetadataPath(), saveFileName)); + + // TODO: Add some error to the user: return BadRequest("Could not save subtitle, bad path."); + if (internalPath.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal)) + { + savePaths.Add(internalPath); + } + + if (savePaths.Count > 0) + { + await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false); + } + else + { + _logger.LogError("An uploaded subtitle could not be saved because the resulting paths were invalid."); + } } } From 40a1e1924aba735c94a6575e225b8e63524f9714 Mon Sep 17 00:00:00 2001 From: Lenart Kos Date: Fri, 17 Feb 2023 22:40:54 +0100 Subject: [PATCH 50/69] Add rule and tests to fix #9341 Add an additional EpisodeExpression that matches `Series title Season 3 Episode 9 - Episode title.avi` correctly. Fixes #9341 --- Emby.Naming/Common/NamingOptions.cs | 10 +++++++++- tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs | 5 +++++ .../Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 54f62a1570..b9c98c9428 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -338,7 +338,15 @@ namespace Emby.Naming.Common } }, - // This isn't a Kodi naming rule, but the expression below causes false positives, + // This isn't a Kodi naming rule, but the expression below causes false episode numbers for + // Title Season X Episode X naming schemes. + // "Series Season X Episode X - Title.avi", "Series S03 E09.avi", "s3 e9 - Title.avi" + new EpisodeExpression(@".*[\\\/]((?[^\\/]+?)\s)?[Ss](?:eason)?\s*(?[0-9]+)\s+[Ee](?:pisode)?\s*(?[0-9]+).*$") + { + IsNamed = true + }, + + // Not a Kodi rule as well, but the expression below also causes false positives, // so we make sure this one gets tested first. // "Foo Bar 889" new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?[\w\s]+?)\s(?[0-9]{1,4})(-(?[0-9]{2,4}))*[^\\\/x]*$") diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs index 68059f9806..406381f142 100644 --- a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs @@ -73,6 +73,11 @@ namespace Jellyfin.Naming.Tests.TV [InlineData("[BBT-RMX] Ranma ½ - 154 [50AC421A].mkv", 154)] // hyphens in the pre-name info, triple digit episode number [InlineData("Season 2/Episode 21 - 94 Meetings.mp4", 21)] // Title starts with a number [InlineData("/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv", 7)] + [InlineData("Season 3/The Series Season 3 Episode 9 - The title.avi", 9)] + [InlineData("Season 3/The Series S3 E9 - The title.avi", 9)] + [InlineData("Season 3/S003 E009.avi", 9)] + [InlineData("Season 3/Season 3 Episode 9.avi", 9)] + // [InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", 317)] // triple digit episode number // TODO: [InlineData("Season 2/16 12 Some Title.avi", 16)] // TODO: [InlineData("Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", 3)] diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs index af219b1186..7604ddc803 100644 --- a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs +++ b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs @@ -30,6 +30,7 @@ namespace Jellyfin.Naming.Tests.TV [InlineData("/Season 02/Elementary - 02x03-E15 - Ep Name.mp4", false, "Elementary", 2, 3)] [InlineData("/Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", false, "Elementary", 1, 23)] [InlineData("/The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH/The Wonder Years s04e07 Christmas Party NTSC PDTV.avi", false, "The Wonder Years", 4, 7)] + [InlineData("/The.Sopranos/Season 3/The Sopranos Season 3 Episode 09 - The Telltale Moozadell.avi", false, "The Sopranos", 3, 9)] // TODO: [InlineData("/Castle Rock 2x01 Que el rio siga su curso [WEB-DL HULU 1080p h264 Dual DD5.1 Subs].mkv", "Castle Rock", 2, 1)] // TODO: [InlineData("/After Life 1x06 Episodio 6 [WEB-DL NF 1080p h264 Dual DD 5.1 Sub].mkv", "After Life", 1, 6)] // TODO: [InlineData("/Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", "Uchuu Senkan Yamoto 2199", 4, 3)] From a527034ebe31e1aa43c5fd4adb98e8cff871988a Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Fri, 17 Feb 2023 15:16:08 -0700 Subject: [PATCH 51/69] Validate requested user id (#8812) --- Jellyfin.Api/Controllers/ArtistsController.cs | 9 ++- .../Controllers/ChannelsController.cs | 9 ++- Jellyfin.Api/Controllers/DevicesController.cs | 2 + Jellyfin.Api/Controllers/FilterController.cs | 8 +- Jellyfin.Api/Controllers/GenresController.cs | 6 +- .../Controllers/InstantMixController.cs | 22 +++-- Jellyfin.Api/Controllers/ItemsController.cs | 3 +- Jellyfin.Api/Controllers/LibraryController.cs | 22 +++-- Jellyfin.Api/Controllers/LiveTvController.cs | 26 +++--- .../Controllers/MediaInfoController.cs | 5 +- Jellyfin.Api/Controllers/MoviesController.cs | 4 +- .../Controllers/MusicGenresController.cs | 6 +- Jellyfin.Api/Controllers/PersonsController.cs | 7 +- .../Controllers/PlaylistsController.cs | 8 +- .../Controllers/QuickConnectController.cs | 11 +-- Jellyfin.Api/Controllers/SearchController.cs | 5 +- Jellyfin.Api/Controllers/StudiosController.cs | 6 +- Jellyfin.Api/Controllers/TvShowsController.cs | 15 ++-- .../Controllers/UniversalAudioController.cs | 6 +- Jellyfin.Api/Controllers/VideosController.cs | 5 +- Jellyfin.Api/Controllers/YearsController.cs | 6 +- Jellyfin.Api/Helpers/RequestHelpers.cs | 27 +++++++ .../Helpers/RequestHelpersTests.cs | 80 +++++++++++++++++++ .../Controllers/ItemsControllerTests.cs | 4 +- 24 files changed, 232 insertions(+), 70 deletions(-) diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index 11933fd97f..c9d2f67f92 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -118,6 +118,7 @@ public class ArtistsController : BaseJellyfinApiController [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -125,7 +126,7 @@ public class ArtistsController : BaseJellyfinApiController User? user = null; BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); - if (userId.HasValue && !userId.Equals(default)) + if (!userId.Value.Equals(default)) { user = _userManager.GetUserById(userId.Value); } @@ -321,6 +322,7 @@ public class ArtistsController : BaseJellyfinApiController [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -328,7 +330,7 @@ public class ArtistsController : BaseJellyfinApiController User? user = null; BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); - if (userId.HasValue && !userId.Equals(default)) + if (!userId.Value.Equals(default)) { user = _userManager.GetUserById(userId.Value); } @@ -462,11 +464,12 @@ public class ArtistsController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions().AddClientFields(User); var item = _libraryManager.GetArtist(name, dtoOptions); - if (userId.HasValue && !userId.Value.Equals(default)) + if (!userId.Value.Equals(default)) { var user = _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index 42f072f669..b5c4d83462 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -60,11 +60,12 @@ public class ChannelsController : BaseJellyfinApiController [FromQuery] bool? supportsMediaDeletion, [FromQuery] bool? isFavorite) { + userId = RequestHelpers.GetUserId(User, userId); return _channelManager.GetChannels(new ChannelQuery { Limit = limit, StartIndex = startIndex, - UserId = userId ?? Guid.Empty, + UserId = userId.Value, SupportsLatestItems = supportsLatestItems, SupportsMediaDeletion = supportsMediaDeletion, IsFavorite = isFavorite @@ -124,7 +125,8 @@ public class ChannelsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -198,7 +200,8 @@ public class ChannelsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 4978622369..aa0dff2123 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.Api.Helpers; using Jellyfin.Data.Dtos; using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Queries; @@ -48,6 +49,7 @@ public class DevicesController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) { + userId = RequestHelpers.GetUserId(User, userId); return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false); } diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index dd64ff9034..dac07429ff 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -1,5 +1,7 @@ using System; using System.Linq; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; @@ -51,7 +53,8 @@ public class FilterController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -143,7 +146,8 @@ public class FilterController : BaseJellyfinApiController [FromQuery] bool? isSeries, [FromQuery] bool? recursive) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index 711fb4aef1..eb03b514c7 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -90,11 +90,12 @@ public class GenresController : BaseJellyfinApiController [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); - User? user = userId is null || userId.Value.Equals(default) + User? user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -155,6 +156,7 @@ public class GenresController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions() .AddClientFields(User); @@ -170,7 +172,7 @@ public class GenresController : BaseJellyfinApiController item ??= new Genre(); - if (userId is null || userId.Value.Equals(default)) + if (userId.Value.Equals(default)) { return _dtoService.GetBaseItemDto(item, dtoOptions); } diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index 43f09b49a2..4dc2a4253d 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Dto; @@ -74,7 +75,8 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var item = _libraryManager.GetItemById(id); - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -110,7 +112,8 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var album = _libraryManager.GetItemById(id); - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -146,7 +149,8 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var playlist = (Playlist)_libraryManager.GetItemById(id); - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -181,7 +185,8 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -217,7 +222,8 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var item = _libraryManager.GetItemById(id); - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -253,7 +259,8 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var item = _libraryManager.GetItemById(id); - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -326,7 +333,8 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var item = _libraryManager.GetItemById(id); - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 99366e80c7..728e628109 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -240,7 +240,8 @@ public class ItemsController : BaseJellyfinApiController { var isApiKey = User.GetIsApiKey(); // if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method - var user = !isApiKey && userId.HasValue && !userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = !isApiKey && !userId.Value.Equals(default) ? _userManager.GetUserById(userId.Value) ?? throw new ResourceNotFoundException() : null; diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index e8b68c7c33..bf59febed8 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LibraryDtos; using Jellyfin.Data.Entities; @@ -142,12 +143,13 @@ public class LibraryController : BaseJellyfinApiController [FromQuery] Guid? userId, [FromQuery] bool inheritFromParent = false) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var item = itemId.Equals(default) - ? (userId is null || userId.Value.Equals(default) + ? (userId.Value.Equals(default) ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); @@ -208,12 +210,13 @@ public class LibraryController : BaseJellyfinApiController [FromQuery] Guid? userId, [FromQuery] bool inheritFromParent = false) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var item = itemId.Equals(default) - ? (userId is null || userId.Value.Equals(default) + ? (userId.Value.Equals(default) ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); @@ -403,7 +406,8 @@ public class LibraryController : BaseJellyfinApiController [FromQuery] Guid? userId, [FromQuery] bool? isFavorite) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -437,6 +441,7 @@ public class LibraryController : BaseJellyfinApiController public ActionResult> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId) { var item = _libraryManager.GetItemById(itemId); + userId = RequestHelpers.GetUserId(User, userId); if (item is null) { @@ -445,7 +450,7 @@ public class LibraryController : BaseJellyfinApiController var baseItemDtos = new List(); - var user = userId is null || userId.Value.Equals(default) + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -675,8 +680,9 @@ public class LibraryController : BaseJellyfinApiController [FromQuery] int? limit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) { + userId = RequestHelpers.GetUserId(User, userId); var item = itemId.Equals(default) - ? (userId is null || userId.Value.Equals(default) + ? (userId.Value.Equals(default) ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); @@ -691,7 +697,7 @@ public class LibraryController : BaseJellyfinApiController return new QueryResult(); } - var user = userId is null || userId.Value.Equals(default) + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 318ed5c673..96fc91f93c 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -153,6 +153,7 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery] bool enableFavoriteSorting = false, [FromQuery] bool addCurrentProgram = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -161,7 +162,7 @@ public class LiveTvController : BaseJellyfinApiController new LiveTvChannelQuery { ChannelType = type, - UserId = userId ?? Guid.Empty, + UserId = userId.Value, StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, @@ -180,7 +181,7 @@ public class LiveTvController : BaseJellyfinApiController dtoOptions, CancellationToken.None); - var user = userId is null || userId.Value.Equals(default) + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -211,7 +212,8 @@ public class LiveTvController : BaseJellyfinApiController [Authorize(Policy = Policies.LiveTvAccess)] public ActionResult GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var item = channelId.Equals(default) @@ -271,6 +273,7 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery] bool? isLibraryItem, [FromQuery] bool enableTotalRecordCount = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -279,7 +282,7 @@ public class LiveTvController : BaseJellyfinApiController new RecordingQuery { ChannelId = channelId, - UserId = userId ?? Guid.Empty, + UserId = userId.Value, StartIndex = startIndex, Limit = limit, Status = status, @@ -382,7 +385,8 @@ public class LiveTvController : BaseJellyfinApiController [Authorize(Policy = Policies.LiveTvAccess)] public ActionResult> GetRecordingFolders([FromQuery] Guid? userId) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var folders = _liveTvManager.GetRecordingFolders(user); @@ -404,7 +408,8 @@ public class LiveTvController : BaseJellyfinApiController [Authorize(Policy = Policies.LiveTvAccess)] public ActionResult GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var item = recordingId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId); @@ -560,7 +565,8 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool enableTotalRecordCount = true) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -699,7 +705,8 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery] bool? enableUserData, [FromQuery] bool enableTotalRecordCount = true) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -737,7 +744,8 @@ public class LiveTvController : BaseJellyfinApiController [FromRoute, Required] string programId, [FromQuery] Guid? userId) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index ea10dd771f..da24616ff3 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -132,6 +132,7 @@ public class MediaInfoController : BaseJellyfinApiController // Copy params from posted body // TODO clean up when breaking API compatibility. userId ??= playbackInfoDto?.UserId; + userId = RequestHelpers.GetUserId(User, userId); maxStreamingBitrate ??= playbackInfoDto?.MaxStreamingBitrate; startTimeTicks ??= playbackInfoDto?.StartTimeTicks; audioStreamIndex ??= playbackInfoDto?.AudioStreamIndex; @@ -253,10 +254,12 @@ public class MediaInfoController : BaseJellyfinApiController [FromQuery] bool? enableDirectPlay, [FromQuery] bool? enableDirectStream) { + userId ??= openLiveStreamDto?.UserId; + userId = RequestHelpers.GetUserId(User, userId); var request = new LiveStreamRequest { OpenToken = openToken ?? openLiveStreamDto?.OpenToken, - UserId = userId ?? openLiveStreamDto?.UserId ?? Guid.Empty, + UserId = userId.Value, PlaySessionId = playSessionId ?? openLiveStreamDto?.PlaySessionId, MaxStreamingBitrate = maxStreamingBitrate ?? openLiveStreamDto?.MaxStreamingBitrate, StartTimeTicks = startTimeTicks ?? openLiveStreamDto?.StartTimeTicks, diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index a9336f6d24..e1145481fa 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; @@ -67,7 +68,8 @@ public class MoviesController : BaseJellyfinApiController [FromQuery] int categoryLimit = 5, [FromQuery] int itemLimit = 8) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 3db1d89c1d..435457af67 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -90,11 +90,12 @@ public class MusicGenresController : BaseJellyfinApiController [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); - User? user = userId is null || userId.Value.Equals(default) + User? user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -144,6 +145,7 @@ public class MusicGenresController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetMusicGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions().AddClientFields(User); MusicGenre? item; @@ -162,7 +164,7 @@ public class MusicGenresController : BaseJellyfinApiController return NotFound(); } - if (userId.HasValue && !userId.Value.Equals(default)) + if (!userId.Value.Equals(default)) { var user = _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index 5310f50b13..b4c6f490a0 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.Linq; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Dto; @@ -77,11 +78,12 @@ public class PersonsController : BaseJellyfinApiController [FromQuery] Guid? userId, [FromQuery] bool? enableImages = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - User? user = userId is null || userId.Value.Equals(default) + User? user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -117,6 +119,7 @@ public class PersonsController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetPerson([FromRoute, Required] string name, [FromQuery] Guid? userId) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions() .AddClientFields(User); @@ -126,7 +129,7 @@ public class PersonsController : BaseJellyfinApiController return NotFound(); } - if (userId.HasValue && !userId.Value.Equals(default)) + if (!userId.Value.Equals(default)) { var user = _userManager.GetUserById(userId.Value); return _dtoService.GetBaseItemDto(item, dtoOptions, user); diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 79c0d3c7b2..c6dbea5e22 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.PlaylistDtos; using MediaBrowser.Controller.Dto; @@ -81,11 +82,13 @@ public class PlaylistsController : BaseJellyfinApiController ids = createPlaylistRequest?.Ids ?? Array.Empty(); } + userId ??= createPlaylistRequest?.UserId ?? default; + userId = RequestHelpers.GetUserId(User, userId); var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest { Name = name ?? createPlaylistRequest?.Name, ItemIdList = ids, - UserId = userId ?? createPlaylistRequest?.UserId ?? default, + UserId = userId.Value, MediaType = mediaType ?? createPlaylistRequest?.MediaType }).ConfigureAwait(false); @@ -107,7 +110,8 @@ public class PlaylistsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, [FromQuery] Guid? userId) { - await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId ?? Guid.Empty).ConfigureAwait(false); + userId = RequestHelpers.GetUserId(User, userId); + await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId.Value).ConfigureAwait(false); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs index 503b9d3729..d7e54b5b6a 100644 --- a/Jellyfin.Api/Controllers/QuickConnectController.cs +++ b/Jellyfin.Api/Controllers/QuickConnectController.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Net; @@ -116,17 +117,11 @@ public class QuickConnectController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task> AuthorizeQuickConnect([FromQuery, Required] string code, [FromQuery] Guid? userId = null) { - var currentUserId = User.GetUserId(); - var actualUserId = userId ?? currentUserId; - - if (actualUserId.Equals(default) || (!userId.Equals(currentUserId) && !User.IsInRole(UserRoles.Administrator))) - { - return Forbid("Unknown user id"); - } + userId = RequestHelpers.GetUserId(User, userId); try { - return await _quickConnect.AuthorizeRequest(actualUserId, code).ConfigureAwait(false); + return await _quickConnect.AuthorizeRequest(userId.Value, code).ConfigureAwait(false); } catch (AuthenticationException) { diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index a25b43345a..f638c31c39 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -3,6 +3,8 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using Jellyfin.Extensions; @@ -98,6 +100,7 @@ public class SearchController : BaseJellyfinApiController [FromQuery] bool includeStudios = true, [FromQuery] bool includeArtists = true) { + userId = RequestHelpers.GetUserId(User, userId); var result = _searchEngine.GetSearchHints(new SearchQuery { Limit = limit, @@ -108,7 +111,7 @@ public class SearchController : BaseJellyfinApiController IncludePeople = includePeople, IncludeStudios = includeStudios, StartIndex = startIndex, - UserId = userId ?? Guid.Empty, + UserId = userId.Value, IncludeItemTypes = includeItemTypes, ExcludeItemTypes = excludeItemTypes, MediaTypes = mediaTypes, diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs index 21965e956d..f434f60f51 100644 --- a/Jellyfin.Api/Controllers/StudiosController.cs +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -86,11 +86,12 @@ public class StudiosController : BaseJellyfinApiController [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - User? user = userId is null || userId.Value.Equals(default) + User? user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -139,10 +140,11 @@ public class StudiosController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetStudio([FromRoute, Required] string name, [FromQuery] Guid? userId) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions().AddClientFields(User); var item = _libraryManager.GetStudio(name); - if (userId.HasValue && !userId.Equals(default)) + if (!userId.Equals(default)) { var user = _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index b0760f97c7..7d23281f2c 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using Jellyfin.Extensions; @@ -87,6 +88,7 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery] bool disableFirstEpisode = false, [FromQuery] bool enableRewatching = false) { + userId = RequestHelpers.GetUserId(User, userId); var options = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -98,7 +100,7 @@ public class TvShowsController : BaseJellyfinApiController ParentId = parentId, SeriesId = seriesId, StartIndex = startIndex, - UserId = userId ?? Guid.Empty, + UserId = userId.Value, EnableTotalRecordCount = enableTotalRecordCount, DisableFirstEpisode = disableFirstEpisode, NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue, @@ -106,7 +108,7 @@ public class TvShowsController : BaseJellyfinApiController }, options); - var user = userId is null || userId.Value.Equals(default) + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -144,7 +146,8 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -215,7 +218,8 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery] bool? enableUserData, [FromQuery] string? sortBy) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -331,7 +335,8 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 3455215979..12d033ae63 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -106,11 +106,7 @@ public class UniversalAudioController : BaseJellyfinApiController [FromQuery] bool enableRedirection = true) { var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels); - - if (!userId.HasValue || userId.Value.Equals(default)) - { - userId = User.GetUserId(); - } + userId = RequestHelpers.GetUserId(User, userId); _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile); diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 3a61367f72..c0ec646eda 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -104,12 +104,13 @@ public class VideosController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetAdditionalPart([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var item = itemId.Equals(default) - ? (userId is null || userId.Value.Equals(default) + ? (userId.Value.Equals(default) ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index def37cb971..74370db50b 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -85,11 +85,12 @@ public class YearsController : BaseJellyfinApiController [FromQuery] bool recursive = true, [FromQuery] bool? enableImages = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - User? user = userId is null || userId.Value.Equals(default) + User? user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); @@ -171,6 +172,7 @@ public class YearsController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetYear([FromRoute, Required] int year, [FromQuery] Guid? userId) { + userId = RequestHelpers.GetUserId(User, userId); var item = _libraryManager.GetYear(year); if (item is null) { @@ -180,7 +182,7 @@ public class YearsController : BaseJellyfinApiController var dtoOptions = new DtoOptions() .AddClientFields(User); - if (userId.HasValue && !userId.Value.Equals(default)) + if (!userId.Value.Equals(default)) { var user = _userManager.GetUserById(userId.Value); return _dtoService.GetBaseItemDto(item, dtoOptions, user); diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 0b7a4fa1ac..57098edbae 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -11,6 +11,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; @@ -55,6 +56,32 @@ public static class RequestHelpers return result; } + /// + /// Checks if the user can access a user. + /// + /// The for the current request. + /// The user id. + /// A whether the user can access the user. + internal static Guid GetUserId(ClaimsPrincipal claimsPrincipal, Guid? userId) + { + var authenticatedUserId = claimsPrincipal.GetUserId(); + + // UserId not provided, fall back to authenticated user id. + if (userId is null || userId.Value.Equals(default)) + { + return authenticatedUserId; + } + + // User must be administrator to access another user. + var isAdministrator = claimsPrincipal.IsInRole(UserRoles.Administrator); + if (!userId.Value.Equals(authenticatedUserId) && !isAdministrator) + { + throw new SecurityException("Forbidden"); + } + + return userId.Value; + } + /// /// Checks if the user can update an entry. /// diff --git a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs index c4640bd226..2d7741d81a 100644 --- a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs +++ b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs @@ -1,7 +1,11 @@ using System; using System.Collections.Generic; +using System.Globalization; +using System.Security.Claims; +using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Net; using Xunit; namespace Jellyfin.Api.Tests.Helpers @@ -15,6 +19,82 @@ namespace Jellyfin.Api.Tests.Helpers Assert.Equal(expected, RequestHelpers.GetOrderBy(sortBy, requestedSortOrder)); } + [Fact] + public static void GetUserId_IsAdmin() + { + Guid? requestUserId = Guid.NewGuid(); + Guid? authUserId = Guid.NewGuid(); + + var claims = new[] + { + new Claim(InternalClaimTypes.UserId, authUserId.Value.ToString("N", CultureInfo.InvariantCulture)), + new Claim(InternalClaimTypes.IsApiKey, bool.FalseString), + new Claim(ClaimTypes.Role, UserRoles.Administrator) + }; + + var identity = new ClaimsIdentity(claims, string.Empty); + var principal = new ClaimsPrincipal(identity); + + var userId = RequestHelpers.GetUserId(principal, requestUserId); + + Assert.Equal(requestUserId, userId); + } + + [Fact] + public static void GetUserId_IsApiKey_EmptyGuid() + { + Guid? requestUserId = Guid.Empty; + + var claims = new[] + { + new Claim(InternalClaimTypes.IsApiKey, bool.TrueString) + }; + + var identity = new ClaimsIdentity(claims, string.Empty); + var principal = new ClaimsPrincipal(identity); + + var userId = RequestHelpers.GetUserId(principal, requestUserId); + + Assert.Equal(Guid.Empty, userId); + } + + [Fact] + public static void GetUserId_IsApiKey_Null() + { + Guid? requestUserId = null; + + var claims = new[] + { + new Claim(InternalClaimTypes.IsApiKey, bool.TrueString) + }; + + var identity = new ClaimsIdentity(claims, string.Empty); + var principal = new ClaimsPrincipal(identity); + + var userId = RequestHelpers.GetUserId(principal, requestUserId); + + Assert.Equal(Guid.Empty, userId); + } + + [Fact] + public static void GetUserId_IsUser() + { + Guid? requestUserId = Guid.NewGuid(); + Guid? authUserId = Guid.NewGuid(); + + var claims = new[] + { + new Claim(InternalClaimTypes.UserId, authUserId.Value.ToString("N", CultureInfo.InvariantCulture)), + new Claim(InternalClaimTypes.IsApiKey, bool.FalseString), + new Claim(ClaimTypes.Role, UserRoles.User) + }; + + var identity = new ClaimsIdentity(claims, string.Empty); + var principal = new ClaimsPrincipal(identity); + + Assert.Throws(() => RequestHelpers.GetUserId(principal, requestUserId)); + } + public static TheoryData, IReadOnlyList, (string, SortOrder)[]> GetOrderBy_Success_TestData() { var data = new TheoryData, IReadOnlyList, (string, SortOrder)[]>(); diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs index 62b32b92e7..0780029949 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs @@ -22,13 +22,13 @@ public sealed class ItemsControllerTests : IClassFixture Date: Fri, 17 Feb 2023 02:06:29 +0000 Subject: [PATCH 52/69] Translated using Weblate (Spanish (Argentina)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es_AR/ --- Emby.Server.Implementations/Localization/Core/es-AR.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json index 8ad9e8c716..8bd3c5defe 100644 --- a/Emby.Server.Implementations/Localization/Core/es-AR.json +++ b/Emby.Server.Implementations/Localization/Core/es-AR.json @@ -118,11 +118,11 @@ "TaskCleanActivityLog": "Borrar log de actividades", "Undefined": "Indefinido", "Forced": "Forzado", - "Default": "Por Defecto", + "Default": "Predeterminado", "TaskOptimizeDatabaseDescription": "Compacta la base de datos y restaura el espacio libre. Ejecutar esta tarea después de actualizar las librerías o realizar otros cambios que impliquen modificar las bases de datos puede mejorar la performance.", "TaskOptimizeDatabase": "Optimización de base de datos", "External": "Externo", "TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reprodución HLS más precisas. Esta tarea puede durar mucho tiempo.", "TaskKeyframeExtractor": "Extractor de Fotogramas Clave", - "HearingImpaired": "Personas con discapacidad auditiva" + "HearingImpaired": "Discapacidad Auditiva" } From 9911638659b2a3dff3309b190cd26b9b50432f44 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 18 Feb 2023 13:10:40 +0000 Subject: [PATCH 53/69] chore(deps): update dependency prometheus-net.aspnetcore to v8 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index d5f762fe83..4024f0c057 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -53,7 +53,7 @@ - + From 3c5b0e0035a15918e4d3f77c317e01fe08a8912e Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sat, 18 Feb 2023 21:22:45 +0100 Subject: [PATCH 54/69] Fix MusicBrainz default server --- .../Plugins/MusicBrainz/Configuration/PluginConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs index 22229e377d..a97b56743d 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs @@ -8,7 +8,7 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz.Configuration; /// public class PluginConfiguration : BasePluginConfiguration { - private const string DefaultServer = "musicbrainz.org"; + private const string DefaultServer = "https://musicbrainz.org"; private const double DefaultRateLimit = 1.0; From 2f4e43b87f3dcdb16798cbaa430e350bf7bfe131 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sun, 19 Feb 2023 09:30:27 +0100 Subject: [PATCH 55/69] Add migration for MusicBrainz settings --- Jellyfin.Server/Migrations/MigrationRunner.cs | 3 +- .../MigrateMusicBrainzTimeout.cs | 89 +++++++++++++++++++ .../Configuration/PluginConfiguration.cs | 1 - 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index 23fb9e3708..8a6ab79323 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -21,7 +21,8 @@ namespace Jellyfin.Server.Migrations /// private static readonly Type[] _preStartupMigrationTypes = { - typeof(PreStartupRoutines.CreateNetworkConfiguration) + typeof(PreStartupRoutines.CreateNetworkConfiguration), + typeof(PreStartupRoutines.MigrateMusicBrainzTimeout) }; /// diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs new file mode 100644 index 0000000000..14b51bd4c4 --- /dev/null +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs @@ -0,0 +1,89 @@ +using System; +using System.IO; +using System.Xml; +using System.Xml.Serialization; +using Emby.Server.Implementations; +using MediaBrowser.Providers.Plugins.MusicBrainz.Configuration; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations.PreStartupRoutines; + +/// +public class MigrateMusicBrainzTimeout : IMigrationRoutine +{ + private readonly ServerApplicationPaths _applicationPaths; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + /// An instance of the interface. + public MigrateMusicBrainzTimeout(ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory) + { + _applicationPaths = applicationPaths; + _logger = loggerFactory.CreateLogger(); + } + + /// + public Guid Id => Guid.Parse("A6DCACF4-C057-4Ef9-80D3-61CEF9DDB4F0"); + + /// + public string Name => nameof(MigrateMusicBrainzTimeout); + + /// + public bool PerformOnNewInstall => false; + + /// + public void Perform() + { + string path = Path.Combine(_applicationPaths.PluginConfigurationsPath, "Jellyfin.Plugin.MusicBrainz.xml"); + if (!File.Exists(path)) + { + _logger.LogDebug("No MusicBrainz plugin configuration file found, skipping"); + return; + } + + var serverConfigSerializer = new XmlSerializer(typeof(OldMusicBrainzConfiguration), new XmlRootAttribute("PluginConfiguration")); + using var xmlReader = XmlReader.Create(path); + var oldPluginConfiguration = serverConfigSerializer.Deserialize(xmlReader) as OldMusicBrainzConfiguration; + + if (oldPluginConfiguration is not null) + { + var newPluginConfiguration = new PluginConfiguration(); + newPluginConfiguration.Server = oldPluginConfiguration.Server; + newPluginConfiguration.ReplaceArtistName = oldPluginConfiguration.ReplaceArtistName; + var newRateLimit = oldPluginConfiguration.RateLimit / 1000.0; + newPluginConfiguration.RateLimit = newRateLimit < 1.0 ? 1.0 : newRateLimit; + + var pluginConfigurationSerializer = new XmlSerializer(typeof(PluginConfiguration), new XmlRootAttribute("PluginConfiguration")); + var xmlWriterSettings = new XmlWriterSettings { Indent = true }; + using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings); + pluginConfigurationSerializer.Serialize(xmlWriter, newPluginConfiguration); + } + } + +#pragma warning disable + public sealed class OldMusicBrainzConfiguration + { + private string _server = string.Empty; + + private long _rateLimit = 0L; + + public string Server + { + get => _server; + set => _server = value.TrimEnd('/'); + } + + public long RateLimit + { + get => _rateLimit; + set => _rateLimit = value; + } + + public bool ReplaceArtistName { get; set; } + } +#pragma warning restore + +} diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs index a97b56743d..a2f3c63f00 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs @@ -1,5 +1,4 @@ using MediaBrowser.Model.Plugins; -using MetaBrainz.MusicBrainz; namespace MediaBrowser.Providers.Plugins.MusicBrainz.Configuration; From de96fe1f521a9869544f4550fa2e7186f192fcd3 Mon Sep 17 00:00:00 2001 From: rushmash Date: Sun, 19 Feb 2023 13:12:32 +0000 Subject: [PATCH 56/69] Translated using Weblate (Russian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ru/ --- Emby.Server.Implementations/Localization/Core/ru.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index 855223381c..839bbcb6d3 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -16,7 +16,7 @@ "Folders": "Папки", "Genres": "Жанры", "HeaderAlbumArtists": "Исполнители альбома", - "HeaderContinueWatching": "Продолжение просмотра", + "HeaderContinueWatching": "Продолжить просмотр", "HeaderFavoriteAlbums": "Избранные альбомы", "HeaderFavoriteArtists": "Избранные исполнители", "HeaderFavoriteEpisodes": "Избранные эпизоды", From 4baa5346795f171a8aec1ca1875dac11ced5c45b Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sun, 19 Feb 2023 16:16:34 +0100 Subject: [PATCH 57/69] Fix MusicBrainz configuration parsing and update --- .../MusicBrainz/MusicBrainzAlbumProvider.cs | 52 ++++++++++--------- .../MusicBrainz/MusicBrainzArtistProvider.cs | 52 ++++++++++--------- 2 files changed, 56 insertions(+), 48 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs index 34f45f0d57..eb65d3be26 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs @@ -8,8 +8,10 @@ using Jellyfin.Extensions; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Providers; using MediaBrowser.Providers.Music; +using MediaBrowser.Providers.Plugins.MusicBrainz.Configuration; using MetaBrainz.MusicBrainz; using MetaBrainz.MusicBrainz.Interfaces.Entities; using MetaBrainz.MusicBrainz.Interfaces.Searches; @@ -23,8 +25,7 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz; public class MusicBrainzAlbumProvider : IRemoteMetadataProvider, IHasOrder, IDisposable { private readonly ILogger _logger; - private readonly Query _musicBrainzQuery; - private readonly string _musicBrainzDefaultUri = "https://musicbrainz.org"; + private Query _musicBrainzQuery; /// /// Initializes a new instance of the class. @@ -33,29 +34,9 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider logger) { _logger = logger; - - MusicBrainz.Plugin.Instance!.ConfigurationChanged += (_, _) => - { - if (Uri.TryCreate(MusicBrainz.Plugin.Instance.Configuration.Server, UriKind.Absolute, out var server)) - { - Query.DefaultServer = server.Host; - Query.DefaultPort = server.Port; - Query.DefaultUrlScheme = server.Scheme; - } - else - { - // Fallback to official server - _logger.LogWarning("Invalid MusicBrainz server specified, falling back to official server"); - var defaultServer = new Uri(_musicBrainzDefaultUri); - Query.DefaultServer = defaultServer.Host; - Query.DefaultPort = defaultServer.Port; - Query.DefaultUrlScheme = defaultServer.Scheme; - } - - Query.DelayBetweenRequests = MusicBrainz.Plugin.Instance.Configuration.RateLimit; - }; - _musicBrainzQuery = new Query(); + ReloadConfig(null, MusicBrainz.Plugin.Instance!.Configuration); + MusicBrainz.Plugin.Instance!.ConfigurationChanged += ReloadConfig; } /// @@ -64,6 +45,29 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider public int Order => 0; + private void ReloadConfig(object? sender, BasePluginConfiguration e) + { + var configuration = (PluginConfiguration)e; + if (Uri.TryCreate(configuration.Server, UriKind.Absolute, out var server)) + { + Query.DefaultServer = server.DnsSafeHost; + Query.DefaultPort = server.Port; + Query.DefaultUrlScheme = server.Scheme; + } + else + { + // Fallback to official server + _logger.LogWarning("Invalid MusicBrainz server specified, falling back to official server"); + var defaultServer = new Uri(configuration.Server); + Query.DefaultServer = defaultServer.Host; + Query.DefaultPort = defaultServer.Port; + Query.DefaultUrlScheme = defaultServer.Scheme; + } + + Query.DelayBetweenRequests = configuration.RateLimit; + _musicBrainzQuery = new Query(); + } + /// public async Task> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancellationToken) { diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs index 718b5a1c46..9623130b0a 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs @@ -8,8 +8,10 @@ using Jellyfin.Extensions; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Providers; using MediaBrowser.Providers.Music; +using MediaBrowser.Providers.Plugins.MusicBrainz.Configuration; using MetaBrainz.MusicBrainz; using MetaBrainz.MusicBrainz.Interfaces.Entities; using MetaBrainz.MusicBrainz.Interfaces.Searches; @@ -23,8 +25,7 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz; public class MusicBrainzArtistProvider : IRemoteMetadataProvider, IDisposable { private readonly ILogger _logger; - private readonly Query _musicBrainzQuery; - private readonly string _musicBrainzDefaultUri = "https://musicbrainz.org"; + private Query _musicBrainzQuery; /// /// Initializes a new instance of the class. @@ -33,34 +34,37 @@ public class MusicBrainzArtistProvider : IRemoteMetadataProvider logger) { _logger = logger; - - MusicBrainz.Plugin.Instance!.ConfigurationChanged += (_, _) => - { - if (Uri.TryCreate(MusicBrainz.Plugin.Instance.Configuration.Server, UriKind.Absolute, out var server)) - { - Query.DefaultServer = server.Host; - Query.DefaultPort = server.Port; - Query.DefaultUrlScheme = server.Scheme; - } - else - { - // Fallback to official server - _logger.LogWarning("Invalid MusicBrainz server specified, falling back to official server"); - var defaultServer = new Uri(_musicBrainzDefaultUri); - Query.DefaultServer = defaultServer.Host; - Query.DefaultPort = defaultServer.Port; - Query.DefaultUrlScheme = defaultServer.Scheme; - } - - Query.DelayBetweenRequests = MusicBrainz.Plugin.Instance.Configuration.RateLimit; - }; - _musicBrainzQuery = new Query(); + ReloadConfig(null, MusicBrainz.Plugin.Instance!.Configuration); + MusicBrainz.Plugin.Instance!.ConfigurationChanged += ReloadConfig; } /// public string Name => "MusicBrainz"; + private void ReloadConfig(object? sender, BasePluginConfiguration e) + { + var configuration = (PluginConfiguration)e; + if (Uri.TryCreate(configuration.Server, UriKind.Absolute, out var server)) + { + Query.DefaultServer = server.DnsSafeHost; + Query.DefaultPort = server.Port; + Query.DefaultUrlScheme = server.Scheme; + } + else + { + // Fallback to official server + _logger.LogWarning("Invalid MusicBrainz server specified, falling back to official server"); + var defaultServer = new Uri(configuration.Server); + Query.DefaultServer = defaultServer.Host; + Query.DefaultPort = defaultServer.Port; + Query.DefaultUrlScheme = defaultServer.Scheme; + } + + Query.DelayBetweenRequests = configuration.RateLimit; + _musicBrainzQuery = new Query(); + } + /// public async Task> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken) { From 24a7e210c377bf828f21b5812f25c6545f7de006 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sun, 19 Feb 2023 16:52:29 +0100 Subject: [PATCH 58/69] Optimize tryparse * Don't check for null before * Don't try different formats when not needed (NumberFormat.Integer is the fast path) --- Emby.Naming/Audio/AlbumParser.cs | 9 ++--- .../Data/SqliteItemRepository.cs | 2 +- .../LiveTv/Listings/SchedulesDirect.cs | 12 +++---- .../LiveTv/TunerHosts/M3uParser.cs | 35 +++++++------------ .../Extensions/ClaimsPrincipalExtensions.cs | 3 +- Jellyfin.Api/Helpers/StreamingHelpers.cs | 4 +-- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 3 +- Jellyfin.Networking/Manager/NetworkManager.cs | 2 +- .../Routines/MigrateDisplayPreferencesDb.cs | 2 +- MediaBrowser.Common/Net/IPHost.cs | 4 +-- .../LiveTv/LiveTvChannel.cs | 12 +++---- .../MediaEncoding/EncodingHelper.cs | 7 ++-- .../MediaEncoding/EncodingJobInfo.cs | 15 +++----- .../MediaEncoding/JobLogger.cs | 8 ++--- .../Parsers/BaseItemXmlParser.cs | 7 ++-- .../Probing/ProbeResultNormalizer.cs | 33 +++++++---------- MediaBrowser.Model/Dlna/ConditionProcessor.cs | 4 +-- MediaBrowser.Model/Dlna/SortCriteria.cs | 2 +- MediaBrowser.Model/Dlna/StreamBuilder.cs | 23 ++++++------ MediaBrowser.Model/Dlna/StreamInfo.cs | 24 +++---------- .../Manager/MetadataService.cs | 1 - .../Plugins/Omdb/OmdbProvider.cs | 8 ++--- .../Parsers/BaseNfoParser.cs | 7 ++-- 23 files changed, 83 insertions(+), 144 deletions(-) diff --git a/Emby.Naming/Audio/AlbumParser.cs b/Emby.Naming/Audio/AlbumParser.cs index bbfdccc902..86a5641531 100644 --- a/Emby.Naming/Audio/AlbumParser.cs +++ b/Emby.Naming/Audio/AlbumParser.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.IO; using System.Text.RegularExpressions; using Emby.Naming.Common; +using Jellyfin.Extensions; namespace Emby.Naming.Audio { @@ -58,13 +59,7 @@ namespace Emby.Naming.Audio var tmp = trimmedFilename.Slice(prefix.Length).Trim(); - int index = tmp.IndexOf(' '); - if (index != -1) - { - tmp = tmp.Slice(0, index); - } - - if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _)) + if (int.TryParse(tmp.LeftPart(' '), CultureInfo.InvariantCulture, out _)) { return true; } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 602d2a8538..90f03995e8 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -1195,7 +1195,7 @@ namespace Emby.Server.Implementations.Data Path = RestorePath(path.ToString()) }; - if (long.TryParse(dateModified, NumberStyles.Any, CultureInfo.InvariantCulture, out var ticks) + if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks) && ticks >= DateTime.MinValue.Ticks && ticks <= DateTime.MaxValue.Ticks) { diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 3f7914d3bb..b5e742f98f 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -570,15 +570,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings _tokens.TryAdd(username, savedToken); } - if (!string.IsNullOrEmpty(savedToken.Name) && !string.IsNullOrEmpty(savedToken.Value)) + if (!string.IsNullOrEmpty(savedToken.Name) + && long.TryParse(savedToken.Value, CultureInfo.InvariantCulture, out long ticks)) { - if (long.TryParse(savedToken.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out long ticks)) + // If it's under 24 hours old we can still use it + if (DateTime.UtcNow.Ticks - ticks < TimeSpan.FromHours(20).Ticks) { - // If it's under 24 hours old we can still use it - if (DateTime.UtcNow.Ticks - ticks < TimeSpan.FromHours(20).Ticks) - { - return savedToken.Name; - } + return savedToken.Name; } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index a423ec8f48..c18cb00740 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -168,28 +168,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts string numberString = null; string attributeValue; - if (attributes.TryGetValue("tvg-chno", out attributeValue)) + if (attributes.TryGetValue("tvg-chno", out attributeValue) + && double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _)) { - if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) - { - numberString = attributeValue; - } + numberString = attributeValue; } if (!IsValidChannelNumber(numberString)) { if (attributes.TryGetValue("tvg-id", out attributeValue)) { - if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) + if (double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _)) { numberString = attributeValue; } - else if (attributes.TryGetValue("channel-id", out attributeValue)) + else if (attributes.TryGetValue("channel-id", out attributeValue) + && double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _)) { - if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) - { - numberString = attributeValue; - } + numberString = attributeValue; } } @@ -207,7 +203,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { var numberPart = nameInExtInf.Slice(0, numberIndex).Trim(new[] { ' ', '.' }); - if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) + if (double.TryParse(numberPart, CultureInfo.InvariantCulture, out _)) { numberString = numberPart.ToString(); } @@ -255,19 +251,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private static bool IsValidChannelNumber(string numberString) { - if (string.IsNullOrWhiteSpace(numberString) || - string.Equals(numberString, "-1", StringComparison.OrdinalIgnoreCase) || - string.Equals(numberString, "0", StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrWhiteSpace(numberString) + || string.Equals(numberString, "-1", StringComparison.OrdinalIgnoreCase) + || string.Equals(numberString, "0", StringComparison.OrdinalIgnoreCase)) { return false; } - if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) - { - return false; - } - - return true; + return double.TryParse(numberString, CultureInfo.InvariantCulture, out _); } private static string GetChannelName(string extInf, Dictionary attributes) @@ -285,7 +276,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' }); - if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) + if (double.TryParse(numberPart, CultureInfo.InvariantCulture, out _)) { // channel.Number = number.ToString(); nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' }); diff --git a/Jellyfin.Api/Extensions/ClaimsPrincipalExtensions.cs b/Jellyfin.Api/Extensions/ClaimsPrincipalExtensions.cs index 6b3e78d4d1..d2e8eb378b 100644 --- a/Jellyfin.Api/Extensions/ClaimsPrincipalExtensions.cs +++ b/Jellyfin.Api/Extensions/ClaimsPrincipalExtensions.cs @@ -71,8 +71,7 @@ public static class ClaimsPrincipalExtensions public static bool GetIsApiKey(this ClaimsPrincipal user) { var claimValue = GetClaimValue(user, InternalClaimTypes.IsApiKey); - return !string.IsNullOrEmpty(claimValue) - && bool.TryParse(claimValue, out var parsedClaimValue) + return bool.TryParse(claimValue, out var parsedClaimValue) && parsedClaimValue; } diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index d867df86ee..9b5a14c4de 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -337,10 +337,10 @@ public static class StreamingHelpers value = index == -1 ? value.Slice(npt.Length) : value.Slice(npt.Length, index - npt.Length); - if (value.IndexOf(':') == -1) + if (!value.Contains(':')) { // Parses npt times in the format of '417.33' - if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds)) + if (double.TryParse(value, CultureInfo.InvariantCulture, out var seconds)) { return TimeSpan.FromSeconds(seconds).Ticks; } diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 12960f87a2..cd8ac49820 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -457,8 +457,7 @@ public class TranscodingJobHelper : IDisposable var videoCodec = state.ActualOutputVideoCodec; var hardwareAccelerationTypeString = _serverConfigurationManager.GetEncodingOptions().HardwareAccelerationType; HardwareEncodingType? hardwareAccelerationType = null; - if (!string.IsNullOrEmpty(hardwareAccelerationTypeString) - && Enum.TryParse(hardwareAccelerationTypeString, out var parsedHardwareAccelerationType)) + if (Enum.TryParse(hardwareAccelerationTypeString, out var parsedHardwareAccelerationType)) { hardwareAccelerationType = parsedHardwareAccelerationType; } diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 86989bfde6..88332ce393 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -316,7 +316,7 @@ namespace Jellyfin.Networking.Manager /// public string GetBindInterface(string source, out int? port) { - if (!string.IsNullOrEmpty(source) && IPHost.TryParse(source, out IPHost host)) + if (IPHost.TryParse(source, out IPHost host)) { return GetBindInterface(host, out port); } diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs index 4b692d14f0..7c4ffdbc00 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs @@ -130,7 +130,7 @@ namespace Jellyfin.Server.Migrations.Routines SkipForwardLength = dto.CustomPrefs.TryGetValue("skipForwardLength", out var length) && int.TryParse(length, out var skipForwardLength) ? skipForwardLength : 30000, - SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length) && !string.IsNullOrEmpty(length) && int.TryParse(length, out var skipBackwardLength) + SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length) && int.TryParse(length, out var skipBackwardLength) ? skipBackwardLength : 10000, EnableNextVideoInfoOverlay = dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled) && !string.IsNullOrEmpty(enabled) diff --git a/MediaBrowser.Common/Net/IPHost.cs b/MediaBrowser.Common/Net/IPHost.cs index 7cf1b8aa0b..ec76a43b6f 100644 --- a/MediaBrowser.Common/Net/IPHost.cs +++ b/MediaBrowser.Common/Net/IPHost.cs @@ -190,7 +190,7 @@ namespace MediaBrowser.Common.Net /// Object representing the string, if it has successfully been parsed. public static IPHost Parse(string host) { - if (!string.IsNullOrEmpty(host) && IPHost.TryParse(host, out IPHost res)) + if (IPHost.TryParse(host, out IPHost res)) { return res; } @@ -206,7 +206,7 @@ namespace MediaBrowser.Common.Net /// Object representing the string, if it has successfully been parsed. public static IPHost Parse(string host, AddressFamily family) { - if (!string.IsNullOrEmpty(host) && IPHost.TryParse(host, out IPHost res)) + if (IPHost.TryParse(host, out IPHost res)) { if (family == AddressFamily.InterNetwork) { diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs index 9788260420..f11e3c8f68 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; using Jellyfin.Extensions; @@ -105,12 +106,9 @@ namespace MediaBrowser.Controller.LiveTv protected override string CreateSortName() { - if (!string.IsNullOrEmpty(Number)) + if (double.TryParse(Number, CultureInfo.InvariantCulture, out double number)) { - if (double.TryParse(Number, NumberStyles.Any, CultureInfo.InvariantCulture, out double number)) - { - return string.Format(CultureInfo.InvariantCulture, "{0:00000.0}", number) + "-" + (Name ?? string.Empty); - } + return string.Format(CultureInfo.InvariantCulture, "{0:00000.0}", number) + "-" + (Name ?? string.Empty); } return (Number ?? string.Empty) + "-" + (Name ?? string.Empty); @@ -122,9 +120,7 @@ namespace MediaBrowser.Controller.LiveTv } public IEnumerable GetTaggedItems() - { - return new List(); - } + => Enumerable.Empty(); public override List GetMediaSources(bool enablePathSubstitution) { diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 14547d4406..11b17eec32 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1143,7 +1143,7 @@ namespace MediaBrowser.Controller.MediaEncoding public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level) { - if (double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out double requestLevel)) + if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel)) { if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)) @@ -1737,7 +1737,7 @@ namespace MediaBrowser.Controller.MediaEncoding else if (string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) { // hevc_qsv use -level 51 instead of -level 153. - if (double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out double hevcLevel)) + if (double.TryParse(level, CultureInfo.InvariantCulture, out double hevcLevel)) { param += " -level " + (hevcLevel / 3); } @@ -1916,8 +1916,7 @@ namespace MediaBrowser.Controller.MediaEncoding // If a specific level was requested, the source must match or be less than var level = state.GetRequestedLevel(videoStream.Codec); - if (!string.IsNullOrEmpty(level) - && double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out var requestLevel)) + if (double.TryParse(level, CultureInfo.InvariantCulture, out var requestLevel)) { if (!videoStream.Level.HasValue) { diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index 179cabc84a..a6b5416601 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -250,8 +250,7 @@ namespace MediaBrowser.Controller.MediaEncoding } var level = GetRequestedLevel(ActualOutputVideoCodec); - if (!string.IsNullOrEmpty(level) - && double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (double.TryParse(level, CultureInfo.InvariantCulture, out var result)) { return result; } @@ -645,8 +644,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(codec)) { var value = BaseRequest.GetOption(codec, "maxrefframes"); - if (!string.IsNullOrEmpty(value) - && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } @@ -665,8 +663,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(codec)) { var value = BaseRequest.GetOption(codec, "videobitdepth"); - if (!string.IsNullOrEmpty(value) - && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } @@ -685,8 +682,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(codec)) { var value = BaseRequest.GetOption(codec, "audiobitdepth"); - if (!string.IsNullOrEmpty(value) - && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } @@ -700,8 +696,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(codec)) { var value = BaseRequest.GetOption(codec, "audiochannels"); - if (!string.IsNullOrEmpty(value) - && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index d8475f12ae..3b34af4e96 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -86,7 +86,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var rate = parts[i + 1]; - if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val)) + if (float.TryParse(rate, CultureInfo.InvariantCulture, out var val)) { framerate = val; } @@ -95,7 +95,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var rate = part.Split('=', 2)[^1]; - if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val)) + if (float.TryParse(rate, CultureInfo.InvariantCulture, out var val)) { framerate = val; } @@ -127,7 +127,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (scale.HasValue) { - if (long.TryParse(size, NumberStyles.Any, CultureInfo.InvariantCulture, out var val)) + if (long.TryParse(size, CultureInfo.InvariantCulture, out var val)) { bytesTranscoded = val * scale.Value; } @@ -146,7 +146,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (scale.HasValue) { - if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val)) + if (float.TryParse(rate, CultureInfo.InvariantCulture, out var val)) { bitRate = (int)Math.Ceiling(val * scale.Value); } diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index 1030cf0558..c8912807ea 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -169,12 +169,9 @@ namespace MediaBrowser.LocalMetadata.Parsers { var text = reader.ReadElementContentAsString(); - if (!string.IsNullOrEmpty(text)) + if (float.TryParse(text, CultureInfo.InvariantCulture, out var value)) { - if (float.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) - { - item.CriticRating = value; - } + item.CriticRating = value; } break; diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 99310a75dd..8b82795883 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -97,12 +97,9 @@ namespace MediaBrowser.MediaEncoding.Probing { info.Container = NormalizeFormat(data.Format.FormatName); - if (!string.IsNullOrEmpty(data.Format.BitRate)) + if (int.TryParse(data.Format.BitRate, CultureInfo.InvariantCulture, out var value)) { - if (int.TryParse(data.Format.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) - { - info.Bitrate = value; - } + info.Bitrate = value; } } @@ -561,8 +558,8 @@ namespace MediaBrowser.MediaEncoding.Probing } } - if (string.IsNullOrWhiteSpace(name) || - string.IsNullOrWhiteSpace(value)) + if (string.IsNullOrWhiteSpace(name) + || string.IsNullOrWhiteSpace(value)) { return null; } @@ -674,9 +671,9 @@ namespace MediaBrowser.MediaEncoding.Probing stream.Channels = streamInfo.Channels; - if (int.TryParse(streamInfo.SampleRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) + if (int.TryParse(streamInfo.SampleRate, CultureInfo.InvariantCulture, out var sampleRate)) { - stream.SampleRate = value; + stream.SampleRate = sampleRate; } stream.ChannelLayout = ParseChannelLayout(streamInfo.ChannelLayout); @@ -853,22 +850,18 @@ namespace MediaBrowser.MediaEncoding.Probing // Get stream bitrate var bitrate = 0; - if (!string.IsNullOrEmpty(streamInfo.BitRate)) + if (int.TryParse(streamInfo.BitRate, CultureInfo.InvariantCulture, out var value)) { - if (int.TryParse(streamInfo.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) - { - bitrate = value; - } + bitrate = value; } // The bitrate info of FLAC musics and some videos is included in formatInfo. if (bitrate == 0 && formatInfo is not null - && !string.IsNullOrEmpty(formatInfo.BitRate) && (stream.Type == MediaStreamType.Video || (isAudio && stream.Type == MediaStreamType.Audio))) { // If the stream info doesn't have a bitrate get the value from the media format info - if (int.TryParse(formatInfo.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) + if (int.TryParse(formatInfo.BitRate, CultureInfo.InvariantCulture, out value)) { bitrate = value; } @@ -972,8 +965,8 @@ namespace MediaBrowser.MediaEncoding.Probing var parts = (original ?? string.Empty).Split(':'); if (!(parts.Length == 2 - && int.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out var width) - && int.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var height) + && int.TryParse(parts[0], CultureInfo.InvariantCulture, out var width) + && int.TryParse(parts[1], CultureInfo.InvariantCulture, out var height) && width > 0 && height > 0)) { @@ -1117,7 +1110,7 @@ namespace MediaBrowser.MediaEncoding.Probing } var duration = GetDictionaryValue(streamInfo.Tags, "DURATION-eng") ?? GetDictionaryValue(streamInfo.Tags, "DURATION"); - if (!string.IsNullOrEmpty(duration) && TimeSpan.TryParse(duration, out var parsedDuration)) + if (TimeSpan.TryParse(duration, out var parsedDuration)) { return parsedDuration.TotalSeconds; } @@ -1446,7 +1439,7 @@ namespace MediaBrowser.MediaEncoding.Probing // Limit accuracy to milliseconds to match xml saving var secondsString = chapter.StartTime; - if (double.TryParse(secondsString, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds)) + if (double.TryParse(secondsString, CultureInfo.InvariantCulture, out var seconds)) { var ms = Math.Round(TimeSpan.FromSeconds(seconds).TotalMilliseconds); info.StartPositionTicks = TimeSpan.FromMilliseconds(ms).Ticks; diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs index 5734224167..00b406bbe6 100644 --- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs +++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs @@ -136,7 +136,7 @@ namespace MediaBrowser.Model.Dlna return !condition.IsRequired; } - if (int.TryParse(condition.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var expected)) + if (int.TryParse(condition.Value, CultureInfo.InvariantCulture, out var expected)) { switch (condition.Condition) { @@ -212,7 +212,7 @@ namespace MediaBrowser.Model.Dlna return !condition.IsRequired; } - if (double.TryParse(condition.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var expected)) + if (double.TryParse(condition.Value, CultureInfo.InvariantCulture, out var expected)) { switch (condition.Condition) { diff --git a/MediaBrowser.Model/Dlna/SortCriteria.cs b/MediaBrowser.Model/Dlna/SortCriteria.cs index 7fef16e535..7df53c6d19 100644 --- a/MediaBrowser.Model/Dlna/SortCriteria.cs +++ b/MediaBrowser.Model/Dlna/SortCriteria.cs @@ -9,7 +9,7 @@ namespace MediaBrowser.Model.Dlna { public SortCriteria(string sortOrder) { - if (!string.IsNullOrEmpty(sortOrder) && Enum.TryParse(sortOrder, true, out var sortOrderValue)) + if (Enum.TryParse(sortOrder, true, out var sortOrderValue)) { SortOrder = sortOrderValue; } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index eb6d602959..ab81bfb34c 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -551,8 +551,7 @@ namespace MediaBrowser.Model.Dlna } playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo; - if (!string.IsNullOrEmpty(transcodingProfile.MaxAudioChannels) - && int.TryParse(transcodingProfile.MaxAudioChannels, NumberStyles.Any, CultureInfo.InvariantCulture, out int transcodingMaxAudioChannels)) + if (int.TryParse(transcodingProfile.MaxAudioChannels, CultureInfo.InvariantCulture, out int transcodingMaxAudioChannels)) { playlistItem.TranscodingMaxAudioChannels = transcodingMaxAudioChannels; } @@ -1607,7 +1606,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var num)) { if (condition.Condition == ProfileConditionType.Equals) { @@ -1633,7 +1632,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var num)) { if (condition.Condition == ProfileConditionType.Equals) { @@ -1669,7 +1668,7 @@ namespace MediaBrowser.Model.Dlna } } - if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var num)) { if (condition.Condition == ProfileConditionType.Equals) { @@ -1793,7 +1792,7 @@ namespace MediaBrowser.Model.Dlna } } - if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var num)) { if (condition.Condition == ProfileConditionType.Equals) { @@ -1829,7 +1828,7 @@ namespace MediaBrowser.Model.Dlna } } - if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var num)) { if (condition.Condition == ProfileConditionType.Equals) { @@ -1919,7 +1918,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var num)) { if (condition.Condition == ProfileConditionType.Equals) { @@ -1945,7 +1944,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var num)) { if (condition.Condition == ProfileConditionType.Equals) { @@ -1971,7 +1970,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num)) + if (float.TryParse(value, CultureInfo.InvariantCulture, out var num)) { if (condition.Condition == ProfileConditionType.Equals) { @@ -1997,7 +1996,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var num)) { if (condition.Condition == ProfileConditionType.Equals) { @@ -2023,7 +2022,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var num)) { if (condition.Condition == ProfileConditionType.Equals) { diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 3b55099079..93ace43df4 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -922,12 +922,8 @@ namespace MediaBrowser.Model.Dlna public int? GetTargetVideoBitDepth(string codec) { var value = GetOption(codec, "videobitdepth"); - if (string.IsNullOrEmpty(value)) - { - return null; - } - if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } @@ -938,12 +934,8 @@ namespace MediaBrowser.Model.Dlna public int? GetTargetAudioBitDepth(string codec) { var value = GetOption(codec, "audiobitdepth"); - if (string.IsNullOrEmpty(value)) - { - return null; - } - if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } @@ -954,12 +946,8 @@ namespace MediaBrowser.Model.Dlna public double? GetTargetVideoLevel(string codec) { var value = GetOption(codec, "level"); - if (string.IsNullOrEmpty(value)) - { - return null; - } - if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (double.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } @@ -970,12 +958,8 @@ namespace MediaBrowser.Model.Dlna public int? GetTargetRefFrames(string codec) { var value = GetOption(codec, "maxrefframes"); - if (string.IsNullOrEmpty(value)) - { - return null; - } - if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 9f287766af..0605b0bd78 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -151,7 +151,6 @@ namespace MediaBrowser.Providers.Manager ApplySearchResult(id, refreshOptions.SearchResult); } - // await FindIdentities(id, cancellationToken).ConfigureAwait(false); id.IsAutomated = refreshOptions.IsAutomated; var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, cancellationToken).ConfigureAwait(false); diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index 497437bd8a..dfaba6423b 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -98,8 +98,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb // item.VoteCount = voteCount; } - if (!string.IsNullOrEmpty(result.imdbRating) - && float.TryParse(result.imdbRating, NumberStyles.Any, CultureInfo.InvariantCulture, out var imdbRating) + if (float.TryParse(result.imdbRating, CultureInfo.InvariantCulture, out var imdbRating) && imdbRating >= 0) { item.CommunityRating = imdbRating; @@ -209,8 +208,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb // item.VoteCount = voteCount; } - if (!string.IsNullOrEmpty(result.imdbRating) - && float.TryParse(result.imdbRating, NumberStyles.Any, CultureInfo.InvariantCulture, out var imdbRating) + if (float.TryParse(result.imdbRating, CultureInfo.InvariantCulture, out var imdbRating) && imdbRating >= 0) { item.CommunityRating = imdbRating; @@ -552,7 +550,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb if (rating?.Value is not null) { var value = rating.Value.TrimEnd('%'); - if (float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var score)) + if (float.TryParse(value, CultureInfo.InvariantCulture, out var score)) { return score; } diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index c3a735c6de..159b8d6580 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -315,12 +315,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers { var text = reader.ReadElementContentAsString(); - if (!string.IsNullOrEmpty(text)) + if (float.TryParse(text, CultureInfo.InvariantCulture, out var value)) { - if (float.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) - { - item.CriticRating = value; - } + item.CriticRating = value; } break; From 3c921e25da2efe5cb3c2fca74c70efc78618097d Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sun, 19 Feb 2023 17:54:59 +0100 Subject: [PATCH 59/69] Fix MusicBrainz album queries and releasegroup handling --- .../MusicBrainz/MusicBrainzAlbumProvider.cs | 38 +++++++++++++------ .../MusicBrainz/MusicBrainzArtistProvider.cs | 3 +- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs index eb65d3be26..3afa90baeb 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs @@ -76,13 +76,13 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider 0) + // Add artists and use first as album artist + var artists = releaseSearchResult.ArtistCredit; + if (artists is not null && artists.Count > 0) { - searchResult.AlbumArtist = new RemoteSearchResult - { - SearchProviderName = Name, - Name = releaseSearchResult.ArtistCredit[0].Name - }; + var artistResults = new List(); - if (releaseSearchResult.ArtistCredit[0].Artist?.Id is not null) + foreach (var artist in artists) { - searchResult.AlbumArtist.SetProviderId(MetadataProvider.MusicBrainzArtist, releaseSearchResult.ArtistCredit[0].Artist!.Id.ToString()); + var artistResult = new RemoteSearchResult + { + Name = artist.Name + }; + + if (artist.Artist?.Id is not null) + { + artistResult.SetProviderId(MetadataProvider.MusicBrainzArtist, artist.Artist!.Id.ToString()); + } + + artistResults.Add(artistResult); } + + searchResult.AlbumArtist = artistResults[0]; + searchResult.Artists = artistResults.ToArray(); } searchResult.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseSearchResult.Id.ToString()); diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs index 9623130b0a..be1d876757 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs @@ -116,7 +116,8 @@ public class MusicBrainzArtistProvider : IRemoteMetadataProvider Date: Sun, 19 Feb 2023 18:12:28 +0100 Subject: [PATCH 60/69] Fix MusicBrainz config page input validation --- .../Plugins/MusicBrainz/Configuration/config.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html index 6f1296bb77..0423d45669 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html @@ -13,8 +13,8 @@
This can be a mirror of the official server or even a custom server.
- -
Span of time between requests in milliseconds. The official server is limited to one request every two seconds.
+ +
Span of time between requests in seconds. The official server is limited to one request every seconds.
public class PluginConfiguration : BasePluginConfiguration { - private const string DefaultServer = "https://musicbrainz.org"; + /// + /// The default server URL. + /// + public const string DefaultServer = "https://musicbrainz.org"; - private const double DefaultRateLimit = 1.0; + /// + /// The default rate limit. + /// + public const double DefaultRateLimit = 1.0; private string _server = DefaultServer; private double _rateLimit = DefaultRateLimit; /// - /// Gets or sets the server url. + /// Gets or sets the server URL. /// public string Server { diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html index 62d86cd8fa..24f2ac0ca9 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html @@ -1,9 +1,14 @@ -
-
-
-

MusicBrainz

- + + + + MusicBrainz + + +
+
+
+

MusicBrainz

+
This can be a mirror of the official server or even a custom server.
@@ -28,7 +33,7 @@ uniquePluginId: "8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a" }; - document.querySelector('.musicBrainzConfigurationPage') + document.querySelector('.configPage') .addEventListener('pageshow', function () { Dashboard.showLoadingMsg(); ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) { @@ -52,7 +57,7 @@ }); }); - document.querySelector('.musicBrainzConfigurationForm') + document.querySelector('.configForm') .addEventListener('submit', function (e) { Dashboard.showLoadingMsg(); diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs index 3afa90baeb..4aa4269891 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs @@ -58,7 +58,7 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProviderOMDb -
+
+

OMDb

- private static readonly Regex _seriesNameRegex = new Regex(@"((?[^\._]{2,})[\._]*)|([\._](?[^\._]{2,}))"); + private static readonly Regex _seriesNameRegex = new Regex(@"((?[^\._]{2,})[\._]*)|([\._](?[^\._]{2,}))", RegexOptions.Compiled); /// /// Resolve information about series from path. diff --git a/Emby.Naming/Video/FileStackRule.cs b/Emby.Naming/Video/FileStackRule.cs index 76b487f428..be0f79d33a 100644 --- a/Emby.Naming/Video/FileStackRule.cs +++ b/Emby.Naming/Video/FileStackRule.cs @@ -17,7 +17,7 @@ public class FileStackRule /// Whether the file stack rule uses numerical or alphabetical numbering. public FileStackRule(string token, bool isNumerical) { - _tokenRegex = new Regex(token, RegexOptions.IgnoreCase); + _tokenRegex = new Regex(token, RegexOptions.IgnoreCase | RegexOptions.Compiled); IsNumerical = isNumerical; } diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index 8247c374d0..6209cd46f4 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using Emby.Naming.Common; +using Jellyfin.Extensions; using MediaBrowser.Model.IO; namespace Emby.Naming.Video @@ -13,6 +14,8 @@ namespace Emby.Naming.Video /// public static class VideoListResolver { + private static readonly Regex _resolutionRegex = new Regex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase | RegexOptions.Compiled); + /// /// Resolves alternative versions and extras from list of video files. /// @@ -115,19 +118,34 @@ namespace Emby.Naming.Video continue; } - if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions)) + if (!IsEligibleForMultiVersion(folderName, video.Files[0].FileNameWithoutExtension, namingOptions)) { return videos; } - if (folderName.Equals(Path.GetFileNameWithoutExtension(video.Files[0].Path.AsSpan()), StringComparison.Ordinal)) + if (folderName.Equals(video.Files[0].FileNameWithoutExtension, StringComparison.Ordinal)) { primary = video; } } - // The list is created and overwritten in the caller, so we are allowed to do in-place sorting - videos.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal)); + if (videos.Count > 1) + { + var groups = videos.GroupBy(x => _resolutionRegex.IsMatch(x.Files[0].FileNameWithoutExtension)).ToList(); + videos.Clear(); + foreach (var group in groups) + { + if (group.Key) + { + videos.InsertRange(0, group.OrderByDescending(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator())); + } + else + { + videos.AddRange(group.OrderBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator())); + } + } + } + primary ??= videos[0]; videos.Remove(primary); @@ -161,9 +179,8 @@ namespace Emby.Naming.Video return true; } - private static bool IsEligibleForMultiVersion(ReadOnlySpan folderName, string testFilePath, NamingOptions namingOptions) + private static bool IsEligibleForMultiVersion(ReadOnlySpan folderName, ReadOnlySpan testFilename, NamingOptions namingOptions) { - var testFilename = Path.GetFileNameWithoutExtension(testFilePath.AsSpan()); if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase)) { return false; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs index 80d9d07247..3450f971fc 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs @@ -13,8 +13,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun public LegacyHdHomerunChannelCommands(string url) { // parse url for channel and program - var regExp = new Regex(@"\/ch([0-9]+)-?([0-9]*)"); - var match = regExp.Match(url); + var match = Regex.Match(url, @"\/ch([0-9]+)-?([0-9]*)"); if (match.Success) { _channel = match.Groups[1].Value; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index 1c0ed6505c..046be7c5c7 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -308,8 +308,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); - var reg = new Regex(@"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase); - var matches = reg.Matches(line); + var matches = Regex.Matches(line, @"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase); remaining = line; diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs index f30b639459..7c61248757 100644 --- a/src/Jellyfin.Extensions/StringExtensions.cs +++ b/src/Jellyfin.Extensions/StringExtensions.cs @@ -12,7 +12,7 @@ namespace Jellyfin.Extensions { // Matches non-conforming unicode chars // https://mnaoumov.wordpress.com/2014/06/14/stripping-invalid-characters-from-utf-16-strings/ - private static readonly Regex _nonConformingUnicode = new Regex("([\ud800-\udbff](?![\udc00-\udfff]))|((? /// Removes the diacritics character from the strings. diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs index 02e6f6368c..294f11ee74 100644 --- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs @@ -188,8 +188,7 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Iron Man/Iron Man-bluray.mkv", @"/movies/Iron Man/Iron Man-3d.mkv", @"/movies/Iron Man/Iron Man-3d-hsbs.mkv", - @"/movies/Iron Man/Iron Man-3d.hsbs.mkv", - @"/movies/Iron Man/Iron Man[test].mkv", + @"/movies/Iron Man/Iron Man[test].mkv" }; var result = VideoListResolver.Resolve( @@ -197,10 +196,14 @@ namespace Jellyfin.Naming.Tests.Video _namingOptions).ToList(); Assert.Single(result); - Assert.Equal(7, result[0].AlternateVersions.Count); - Assert.False(result[0].AlternateVersions[2].Is3D); - Assert.True(result[0].AlternateVersions[3].Is3D); - Assert.True(result[0].AlternateVersions[4].Is3D); + Assert.Equal("/movies/Iron Man/Iron Man.mkv", result[0].Files[0].Path); + Assert.Equal(6, result[0].AlternateVersions.Count); + Assert.Equal("/movies/Iron Man/Iron Man-720p.mkv", result[0].AlternateVersions[0].Path); + Assert.Equal("/movies/Iron Man/Iron Man-3d.mkv", result[0].AlternateVersions[1].Path); + Assert.Equal("/movies/Iron Man/Iron Man-3d-hsbs.mkv", result[0].AlternateVersions[2].Path); + Assert.Equal("/movies/Iron Man/Iron Man-bluray.mkv", result[0].AlternateVersions[3].Path); + Assert.Equal("/movies/Iron Man/Iron Man-test.mkv", result[0].AlternateVersions[4].Path); + Assert.Equal("/movies/Iron Man/Iron Man[test].mkv", result[0].AlternateVersions[5].Path); } [Fact] @@ -214,7 +217,6 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Iron Man/Iron Man - bluray.mkv", @"/movies/Iron Man/Iron Man - 3d.mkv", @"/movies/Iron Man/Iron Man - 3d-hsbs.mkv", - @"/movies/Iron Man/Iron Man - 3d.hsbs.mkv", @"/movies/Iron Man/Iron Man [test].mkv" }; @@ -223,10 +225,14 @@ namespace Jellyfin.Naming.Tests.Video _namingOptions).ToList(); Assert.Single(result); - Assert.Equal(7, result[0].AlternateVersions.Count); - Assert.False(result[0].AlternateVersions[3].Is3D); - Assert.True(result[0].AlternateVersions[4].Is3D); - Assert.True(result[0].AlternateVersions[5].Is3D); + Assert.Equal("/movies/Iron Man/Iron Man.mkv", result[0].Files[0].Path); + Assert.Equal(6, result[0].AlternateVersions.Count); + Assert.Equal("/movies/Iron Man/Iron Man - 720p.mkv", result[0].AlternateVersions[0].Path); + Assert.Equal("/movies/Iron Man/Iron Man - 3d.mkv", result[0].AlternateVersions[1].Path); + Assert.Equal("/movies/Iron Man/Iron Man - 3d-hsbs.mkv", result[0].AlternateVersions[2].Path); + Assert.Equal("/movies/Iron Man/Iron Man - bluray.mkv", result[0].AlternateVersions[3].Path); + Assert.Equal("/movies/Iron Man/Iron Man - test.mkv", result[0].AlternateVersions[4].Path); + Assert.Equal("/movies/Iron Man/Iron Man [test].mkv", result[0].AlternateVersions[5].Path); } [Fact] @@ -328,8 +334,12 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { + @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Theatrical Release.mkv", + @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Directors Cut.mkv", @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv", - @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv" + @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p.mkv", + @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p.mkv", + @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv", }; var result = VideoListResolver.Resolve( @@ -338,8 +348,12 @@ namespace Jellyfin.Naming.Tests.Video Assert.Single(result); Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv", result[0].Files[0].Path); - Assert.Single(result[0].AlternateVersions); - Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv", result[0].AlternateVersions[0].Path); + Assert.Equal(5, result[0].AlternateVersions.Count); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p.mkv", result[0].AlternateVersions[0].Path); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv", result[0].AlternateVersions[1].Path); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p.mkv", result[0].AlternateVersions[2].Path); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Directors Cut.mkv", result[0].AlternateVersions[3].Path); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Theatrical Release.mkv", result[0].AlternateVersions[4].Path); } [Fact] From cf29e9a9c587055381d839d9afeb5593a5dcd683 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Mon, 20 Feb 2023 20:32:49 +0100 Subject: [PATCH 65/69] Fix #7516 --- Emby.Dlna/Server/DescriptionXmlBuilder.cs | 26 ++++++---- .../Server/DescriptionXmlBuilderTests.cs | 47 +++++++++++++++++++ 2 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 tests/Jellyfin.Dlna.Tests/Server/DescriptionXmlBuilderTests.cs diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs index d00df781d6..69ef6f6456 100644 --- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs +++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs @@ -147,11 +147,16 @@ namespace Emby.Dlna.Server } } - private string GetFriendlyName() + internal string GetFriendlyName() { if (string.IsNullOrEmpty(_profile.FriendlyName)) { - return "Jellyfin - " + _serverName; + return _serverName; + } + + if (!_profile.FriendlyName.Contains("${HostName}", StringComparison.OrdinalIgnoreCase)) + { + return _profile.FriendlyName; } var characterList = new List(); @@ -164,13 +169,18 @@ namespace Emby.Dlna.Server } } - var characters = characterList.ToArray(); + var serverName = string.Create( + characterList.Count, + characterList, + (dest, source) => + { + for (int i = 0; i < dest.Length; i++) + { + dest[i] = source[i]; + } + }); - var serverName = new string(characters); - - var name = _profile.FriendlyName?.Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase); - - return name ?? string.Empty; + return _profile.FriendlyName.Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase); } private void AppendIconList(StringBuilder builder) diff --git a/tests/Jellyfin.Dlna.Tests/Server/DescriptionXmlBuilderTests.cs b/tests/Jellyfin.Dlna.Tests/Server/DescriptionXmlBuilderTests.cs new file mode 100644 index 0000000000..c9018fe2f4 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Server/DescriptionXmlBuilderTests.cs @@ -0,0 +1,47 @@ +using Emby.Dlna.Server; +using MediaBrowser.Model.Dlna; +using Xunit; + +namespace Jellyfin.Dlna.Server.Tests; + +public class DescriptionXmlBuilderTests +{ + [Fact] + public void GetFriendlyName_EmptyProfile_ReturnsServerName() + { + const string ServerName = "Test Server Name"; + var builder = new DescriptionXmlBuilder(new DeviceProfile(), "serverUdn", "localhost", ServerName, string.Empty); + Assert.Equal(ServerName, builder.GetFriendlyName()); + } + + [Fact] + public void GetFriendlyName_FriendlyName_ReturnsFriendlyName() + { + const string FriendlyName = "Friendly Neighborhood Test Server"; + var builder = new DescriptionXmlBuilder( + new DeviceProfile() + { + FriendlyName = FriendlyName + }, + "serverUdn", + "localhost", + "Test Server Name", + string.Empty); + Assert.Equal(FriendlyName, builder.GetFriendlyName()); + } + + [Fact] + public void GetFriendlyName_FriendlyNameInterpolation_ReturnsFriendlyName() + { + var builder = new DescriptionXmlBuilder( + new DeviceProfile() + { + FriendlyName = "Friendly Neighborhood ${HostName}" + }, + "serverUdn", + "localhost", + "Test Server Name", + string.Empty); + Assert.Equal("Friendly Neighborhood TestServerName", builder.GetFriendlyName()); + } +} From 37560784670ea12b63bfc0d573c42e9599f776da Mon Sep 17 00:00:00 2001 From: Asahi Oka Date: Sun, 19 Feb 2023 20:55:16 +0000 Subject: [PATCH 66/69] Translated using Weblate (Spanish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es/ --- Emby.Server.Implementations/Localization/Core/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index afffdf3bfa..5e41462db1 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -31,7 +31,7 @@ "ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca", "LabelIpAddressValue": "Dirección IP: {0}", "LabelRunningTimeValue": "Tiempo de funcionamiento: {0}", - "Latest": "Últimos", + "Latest": "Último contenido en", "MessageApplicationUpdated": "Se ha actualizado el servidor Jellyfin", "MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}", "MessageNamedServerConfigurationUpdatedWithValue": "La sección {0} de configuración del servidor ha sido actualizada", From e148f9e7173b4c6aba0096f5cc91de976664c60f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Feb 2023 14:29:49 +0000 Subject: [PATCH 67/69] chore(deps): update dependency microsoft.net.test.sdk to v17.5.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4024f0c057..da8f636e50 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -45,7 +45,7 @@ - + From 88ab6bfdfc010ad7b42a01ac1f65b7df4a846c2a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Feb 2023 21:28:46 -0700 Subject: [PATCH 68/69] chore(deps): update dependency autofixture to v4.18.0 (#9370) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index da8f636e50..5060b3de56 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,7 +8,7 @@ - + From 0af699621aa4794866c24ed9315318ebee66356a Mon Sep 17 00:00:00 2001 From: lyaschuchenko Date: Tue, 21 Feb 2023 05:46:11 +0000 Subject: [PATCH 69/69] Translated using Weblate (Ukrainian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/uk/ --- Emby.Server.Implementations/Localization/Core/uk.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json index 92ce616f2e..ff77fb8c56 100644 --- a/Emby.Server.Implementations/Localization/Core/uk.json +++ b/Emby.Server.Implementations/Localization/Core/uk.json @@ -86,7 +86,7 @@ "Shows": "Шоу", "ServerNameNeedsToBeRestarted": "{0} потрібно перезапустити", "ScheduledTaskStartedWithName": "{0} розпочато", - "ScheduledTaskFailedWithName": "Помилка {0}", + "ScheduledTaskFailedWithName": "{0} незавершено, збій", "ProviderValue": "Постачальник: {0}", "PluginUpdatedWithName": "{0} оновлено", "PluginUninstalledWithName": "{0} видалено",