From 86b77de5229c31bfc4ea77a2d0b4b72cfac64c14 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Mon, 4 Mar 2024 17:06:38 -0700 Subject: [PATCH 001/444] Don't decode animated images --- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index a407194992..92299dd063 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -262,6 +262,11 @@ public class SkiaEncoder : IImageEncoder return null; } + if (codec.FrameCount != 0) + { + throw new ArgumentException("Cannot decode images with multiple frames"); + } + // create the bitmap var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack); From 88b3490d1756236d0c2fc00243420d45d149a5d1 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 26 Mar 2024 15:29:48 +0100 Subject: [PATCH 002/444] Add playlist ACL endpoints --- .../Playlists/PlaylistManager.cs | 88 ++++++++----- .../Controllers/PlaylistsController.cs | 122 ++++++++++++++++++ .../Migrations/Routines/FixPlaylistOwner.cs | 2 +- .../Playlists/IPlaylistManager.cs | 35 +++++ MediaBrowser.Controller/Playlists/Playlist.cs | 29 ++--- MediaBrowser.Model/Entities/IHasShares.cs | 6 +- 6 files changed, 235 insertions(+), 47 deletions(-) diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index aea8d65322..6724d54d1a 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -59,6 +59,11 @@ namespace Emby.Server.Implementations.Playlists _appConfig = appConfig; } + public Playlist GetPlaylist(Guid userId, Guid playlistId) + { + return GetPlaylists(userId).Where(p => p.Id.Equals(playlistId)).FirstOrDefault(); + } + public IEnumerable GetPlaylists(Guid userId) { var user = _userManager.GetUserById(userId); @@ -160,7 +165,7 @@ namespace Emby.Server.Implementations.Playlists } } - private string GetTargetPath(string path) + private static string GetTargetPath(string path) { while (Directory.Exists(path)) { @@ -231,13 +236,8 @@ namespace Emby.Server.Implementations.Playlists // Update the playlist in the repository playlist.LinkedChildren = newLinkedChildren; - await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); - // Update the playlist on disk - if (playlist.IsFile) - { - SavePlaylistFile(playlist); - } + await UpdatePlaylist(playlist).ConfigureAwait(false); // Refresh playlist metadata _providerManager.QueueRefresh( @@ -266,12 +266,7 @@ namespace Emby.Server.Implementations.Playlists .Select(i => i.Item1) .ToArray(); - await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); - - if (playlist.IsFile) - { - SavePlaylistFile(playlist); - } + await UpdatePlaylist(playlist).ConfigureAwait(false); _providerManager.QueueRefresh( playlist.Id, @@ -313,14 +308,9 @@ namespace Emby.Server.Implementations.Playlists newList.Insert(newIndex, item); } - playlist.LinkedChildren = newList.ToArray(); + playlist.LinkedChildren = [.. newList]; - await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); - - if (playlist.IsFile) - { - SavePlaylistFile(playlist); - } + await UpdatePlaylist(playlist).ConfigureAwait(false); } /// @@ -430,8 +420,11 @@ namespace Emby.Server.Implementations.Playlists } else if (extension.Equals(".m3u8", StringComparison.OrdinalIgnoreCase)) { - var playlist = new M3uPlaylist(); - playlist.IsExtended = true; + var playlist = new M3uPlaylist + { + IsExtended = true + }; + foreach (var child in item.GetLinkedChildren()) { var entry = new M3uPlaylistEntry() @@ -481,7 +474,7 @@ namespace Emby.Server.Implementations.Playlists } } - private string NormalizeItemPath(string playlistPath, string itemPath) + private static string NormalizeItemPath(string playlistPath, string itemPath) { return MakeRelativePath(Path.GetDirectoryName(playlistPath), itemPath); } @@ -541,12 +534,7 @@ namespace Emby.Server.Implementations.Playlists { playlist.OwnerUserId = guid; playlist.Shares = rankedShares.Skip(1).ToArray(); - await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); - - if (playlist.IsFile) - { - SavePlaylistFile(playlist); - } + await UpdatePlaylist(playlist).ConfigureAwait(false); } else if (!playlist.OpenAccess) { @@ -563,5 +551,47 @@ namespace Emby.Server.Implementations.Playlists } } } + + public async Task ToggleOpenAccess(Guid playlistId, Guid userId) + { + var playlist = GetPlaylist(userId, playlistId); + playlist.OpenAccess = !playlist.OpenAccess; + + await UpdatePlaylist(playlist).ConfigureAwait(false); + } + + public async Task AddToShares(Guid playlistId, Guid userId, Share share) + { + var playlist = GetPlaylist(userId, playlistId); + var shares = playlist.Shares.ToList(); + var existingUserShare = shares.FirstOrDefault(s => s.UserId?.Equals(share.UserId, StringComparison.OrdinalIgnoreCase) ?? false); + if (existingUserShare is not null) + { + shares.Remove(existingUserShare); + } + + shares.Add(share); + playlist.Shares = shares; + await UpdatePlaylist(playlist).ConfigureAwait(false); + } + + public async Task RemoveFromShares(Guid playlistId, Guid userId, Share share) + { + var playlist = GetPlaylist(userId, playlistId); + var shares = playlist.Shares.ToList(); + shares.Remove(share); + playlist.Shares = shares; + await UpdatePlaylist(playlist).ConfigureAwait(false); + } + + private async Task UpdatePlaylist(Playlist playlist) + { + await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); + + if (playlist.IsFile) + { + SavePlaylistFile(playlist); + } + } } } diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 0e7c3f1556..f0e8227fda 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -98,6 +98,128 @@ public class PlaylistsController : BaseJellyfinApiController return result; } + /// + /// Get a playlist's shares. + /// + /// The playlist id. + /// + /// A list of objects. + /// + [HttpGet("{playlistId}/Shares")] + [ProducesResponseType(StatusCodes.Status200OK)] + public IReadOnlyList GetPlaylistShares( + [FromRoute, Required] Guid playlistId) + { + var userId = RequestHelpers.GetUserId(User, default); + + var playlist = _playlistManager.GetPlaylist(userId, playlistId); + var isPermitted = playlist.OwnerUserId.Equals(userId) + || playlist.Shares.Any(s => s.CanEdit && (s.UserId?.Equals(userId) ?? false)); + + return isPermitted ? playlist.Shares : new List(); + } + + /// + /// Toggles OpenAccess of a playlist. + /// + /// The playlist id. + /// + /// A that represents the asynchronous operation to toggle OpenAccess of a playlist. + /// The task result contains an indicating success. + /// + [HttpPost("{playlistId}/ToggleOpenAccess")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task ToggleopenAccess( + [FromRoute, Required] Guid playlistId) + { + var callingUserId = RequestHelpers.GetUserId(User, default); + + var playlist = _playlistManager.GetPlaylist(callingUserId, playlistId); + var isPermitted = playlist.OwnerUserId.Equals(callingUserId) + || playlist.Shares.Any(s => s.CanEdit && (s.UserId?.Equals(callingUserId) ?? false)); + + if (!isPermitted) + { + return Unauthorized("Unauthorized access"); + } + + await _playlistManager.ToggleOpenAccess(playlistId, callingUserId).ConfigureAwait(false); + + return NoContent(); + } + + /// + /// Adds shares to a playlist's shares. + /// + /// The playlist id. + /// The shares. + /// + /// A that represents the asynchronous operation to add shares to a playlist. + /// The task result contains an indicating success. + /// + [HttpPost("{playlistId}/Shares")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task AddUserToPlaylistShares( + [FromRoute, Required] Guid playlistId, + [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Disallow)] Share[] shares) + { + var callingUserId = RequestHelpers.GetUserId(User, default); + + var playlist = _playlistManager.GetPlaylist(callingUserId, playlistId); + var isPermitted = playlist.OwnerUserId.Equals(callingUserId) + || playlist.Shares.Any(s => s.CanEdit && (s.UserId?.Equals(callingUserId) ?? false)); + + if (!isPermitted) + { + return Unauthorized("Unauthorized access"); + } + + foreach (var share in shares) + { + await _playlistManager.AddToShares(playlistId, callingUserId, share).ConfigureAwait(false); + } + + return NoContent(); + } + + /// + /// Remove a user from a playlist's shares. + /// + /// The playlist id. + /// The user id. + /// + /// A that represents the asynchronous operation to delete a user from a playlist's shares. + /// The task result contains an indicating success. + /// + [HttpDelete("{playlistId}/Shares")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task RemoveUserFromPlaylistShares( + [FromRoute, Required] Guid playlistId, + [FromBody] Guid userId) + { + var callingUserId = RequestHelpers.GetUserId(User, default); + + var playlist = _playlistManager.GetPlaylist(callingUserId, playlistId); + var isPermitted = playlist.OwnerUserId.Equals(callingUserId) + || playlist.Shares.Any(s => s.CanEdit && (s.UserId?.Equals(callingUserId) ?? false)); + + if (!isPermitted) + { + return Unauthorized("Unauthorized access"); + } + + var share = playlist.Shares.FirstOrDefault(s => s.UserId?.Equals(userId) ?? false); + + if (share is null) + { + return NotFound(); + } + + await _playlistManager.RemoveFromShares(playlistId, callingUserId, share).ConfigureAwait(false); + + return NoContent(); + } + /// /// Adds items to a playlist. /// diff --git a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs index cf31820034..06596c171f 100644 --- a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs +++ b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs @@ -54,7 +54,7 @@ internal class FixPlaylistOwner : IMigrationRoutine foreach (var playlist in playlists) { var shares = playlist.Shares; - if (shares.Length > 0) + if (shares.Count > 0) { var firstEditShare = shares.First(x => x.CanEdit); if (firstEditShare is not null && Guid.TryParse(firstEditShare.UserId, out var guid)) diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index bb68a3b6dd..aaca1cc492 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -4,12 +4,21 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.Playlists; namespace MediaBrowser.Controller.Playlists { public interface IPlaylistManager { + /// + /// Gets the playlist. + /// + /// The user identifier. + /// The playlist identifier. + /// Playlist. + Playlist GetPlaylist(Guid userId, Guid playlistId); + /// /// Gets the playlists. /// @@ -17,6 +26,32 @@ namespace MediaBrowser.Controller.Playlists /// IEnumerable<Playlist>. IEnumerable GetPlaylists(Guid userId); + /// + /// Toggle OpenAccess policy of the playlist. + /// + /// The playlist identifier. + /// The user identifier. + /// Task. + Task ToggleOpenAccess(Guid playlistId, Guid userId); + + /// + /// Adds a share to the playlist. + /// + /// The playlist identifier. + /// The user identifier. + /// The share. + /// Task. + Task AddToShares(Guid playlistId, Guid userId, Share share); + + /// + /// Rremoves a share from the playlist. + /// + /// The playlist identifier. + /// The user identifier. + /// The share. + /// Task. + Task RemoveFromShares(Guid playlistId, Guid userId, Share share); + /// /// Creates the playlist. /// diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index ca032e7f6e..9a08a4ce3e 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -16,20 +16,19 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Querying; namespace MediaBrowser.Controller.Playlists { public class Playlist : Folder, IHasShares { - public static readonly IReadOnlyList SupportedExtensions = new[] - { + public static readonly IReadOnlyList SupportedExtensions = + [ ".m3u", ".m3u8", ".pls", ".wpl", ".zpl" - }; + ]; public Playlist() { @@ -41,7 +40,7 @@ namespace MediaBrowser.Controller.Playlists public bool OpenAccess { get; set; } - public Share[] Shares { get; set; } + public IReadOnlyList Shares { get; set; } [JsonIgnore] public bool IsFile => IsPlaylistFile(Path); @@ -192,9 +191,9 @@ namespace MediaBrowser.Controller.Playlists return LibraryManager.GetItemList(new InternalItemsQuery(user) { Recursive = true, - IncludeItemTypes = new[] { BaseItemKind.Audio }, - GenreIds = new[] { musicGenre.Id }, - OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) }, + IncludeItemTypes = [BaseItemKind.Audio], + GenreIds = [musicGenre.Id], + OrderBy = [(ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending)], DtoOptions = options }); } @@ -204,9 +203,9 @@ namespace MediaBrowser.Controller.Playlists return LibraryManager.GetItemList(new InternalItemsQuery(user) { Recursive = true, - IncludeItemTypes = new[] { BaseItemKind.Audio }, - ArtistIds = new[] { musicArtist.Id }, - OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) }, + IncludeItemTypes = [BaseItemKind.Audio], + ArtistIds = [musicArtist.Id], + OrderBy = [(ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending)], DtoOptions = options }); } @@ -217,8 +216,8 @@ namespace MediaBrowser.Controller.Playlists { Recursive = true, IsFolder = false, - OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, - MediaTypes = new[] { mediaType }, + OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)], + MediaTypes = [mediaType], EnableTotalRecordCount = false, DtoOptions = options }; @@ -226,7 +225,7 @@ namespace MediaBrowser.Controller.Playlists return folder.GetItemList(query); } - return new[] { item }; + return [item]; } public override bool IsVisible(User user) @@ -248,7 +247,7 @@ namespace MediaBrowser.Controller.Playlists } var shares = Shares; - if (shares.Length == 0) + if (shares.Count == 0) { return false; } diff --git a/MediaBrowser.Model/Entities/IHasShares.cs b/MediaBrowser.Model/Entities/IHasShares.cs index b34d1a0376..31574a3ffa 100644 --- a/MediaBrowser.Model/Entities/IHasShares.cs +++ b/MediaBrowser.Model/Entities/IHasShares.cs @@ -1,4 +1,6 @@ -namespace MediaBrowser.Model.Entities; +using System.Collections.Generic; + +namespace MediaBrowser.Model.Entities; /// /// Interface for access to shares. @@ -8,5 +10,5 @@ public interface IHasShares /// /// Gets or sets the shares. /// - Share[] Shares { get; set; } + IReadOnlyList Shares { get; set; } } From f1dc1610a28fdb2dec4241d233503b6e11020546 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 26 Mar 2024 16:13:07 +0100 Subject: [PATCH 003/444] Extend playlist creation capabilities --- .../Playlists/PlaylistManager.cs | 10 +++------- Jellyfin.Api/Controllers/PlaylistsController.cs | 4 +++- .../Models/PlaylistDtos/CreatePlaylistDto.cs | 13 ++++++++++++- .../Playlists/PlaylistCreationRequest.cs | 7 ++++++- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 6724d54d1a..6b169db794 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -85,12 +85,7 @@ namespace Emby.Server.Implementations.Playlists { foreach (var itemId in options.ItemIdList) { - var item = _libraryManager.GetItemById(itemId); - if (item is null) - { - throw new ArgumentException("No item exists with the supplied Id"); - } - + var item = _libraryManager.GetItemById(itemId) ?? throw new ArgumentException("No item exists with the supplied Id"); if (item.MediaType != MediaType.Unknown) { options.MediaType = item.MediaType; @@ -139,7 +134,8 @@ namespace Emby.Server.Implementations.Playlists Name = name, Path = path, OwnerUserId = options.UserId, - Shares = options.Shares ?? Array.Empty() + Shares = options.Shares ?? [], + OpenAccess = options.OpenAccess ?? false }; playlist.SetMediaType(options.MediaType); diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index f0e8227fda..bf618e8fd7 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -92,7 +92,9 @@ public class PlaylistsController : BaseJellyfinApiController Name = name ?? createPlaylistRequest?.Name, ItemIdList = ids, UserId = userId.Value, - MediaType = mediaType ?? createPlaylistRequest?.MediaType + MediaType = mediaType ?? createPlaylistRequest?.MediaType, + Shares = createPlaylistRequest?.Shares.ToArray(), + OpenAccess = createPlaylistRequest?.OpenAccess }).ConfigureAwait(false); return result; diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs index bdc4888719..a82bff65ec 100644 --- a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs +++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json.Converters; +using MediaBrowser.Model.Entities; namespace Jellyfin.Api.Models.PlaylistDtos; @@ -20,7 +21,7 @@ public class CreatePlaylistDto /// Gets or sets item ids to add to the playlist. /// [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList Ids { get; set; } = Array.Empty(); + public IReadOnlyList Ids { get; set; } = []; /// /// Gets or sets the user id. @@ -31,4 +32,14 @@ public class CreatePlaylistDto /// Gets or sets the media type. /// public MediaType? MediaType { get; set; } + + /// + /// Gets or sets the shares. + /// + public IReadOnlyList Shares { get; set; } = []; + + /// + /// Gets or sets a value indicating whether open access is enabled. + /// + public bool OpenAccess { get; set; } } diff --git a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs index 62d496d047..93eccd5c77 100644 --- a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs +++ b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs @@ -33,5 +33,10 @@ public class PlaylistCreationRequest /// /// Gets or sets the shares. /// - public Share[]? Shares { get; set; } + public IReadOnlyList? Shares { get; set; } + + /// + /// Gets or sets a value indicating whether open access is enabled. + /// + public bool? OpenAccess { get; set; } } From 56c432a8439e1b75c15729bfdb19d41d34e3124d Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 26 Mar 2024 23:45:14 +0100 Subject: [PATCH 004/444] Apply review suggestions --- .../Library/Resolvers/PlaylistResolver.cs | 9 ++-- .../Playlists/PlaylistManager.cs | 14 ++--- .../Controllers/PlaylistsController.cs | 51 +++++++++---------- .../Models/PlaylistDtos/CreatePlaylistDto.cs | 10 ++-- .../Playlists/IPlaylistManager.cs | 4 +- MediaBrowser.Controller/Playlists/Playlist.cs | 10 ++-- .../Parsers/BaseItemXmlParser.cs | 18 +++---- MediaBrowser.Model/Entities/IHasShares.cs | 4 +- MediaBrowser.Model/Entities/Share.cs | 17 ------- .../Entities/UserPermissions.cs | 19 +++++++ .../Playlists/PlaylistCreationRequest.cs | 10 ++-- 11 files changed, 82 insertions(+), 84 deletions(-) delete mode 100644 MediaBrowser.Model/Entities/Share.cs create mode 100644 MediaBrowser.Model/Entities/UserPermissions.cs diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs index a50435ae69..a03c1214d6 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.IO; using System.Linq; @@ -11,7 +9,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Resolvers; using MediaBrowser.LocalMetadata.Savers; -using MediaBrowser.Model.Entities; namespace Emby.Server.Implementations.Library.Resolvers { @@ -20,11 +17,11 @@ namespace Emby.Server.Implementations.Library.Resolvers /// public class PlaylistResolver : GenericFolderResolver { - private CollectionType?[] _musicPlaylistCollectionTypes = - { + private readonly CollectionType?[] _musicPlaylistCollectionTypes = + [ null, CollectionType.music - }; + ]; /// protected override Playlist Resolve(ItemResolveArgs args) diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 6b169db794..59c96852af 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -134,8 +134,8 @@ namespace Emby.Server.Implementations.Playlists Name = name, Path = path, OwnerUserId = options.UserId, - Shares = options.Shares ?? [], - OpenAccess = options.OpenAccess ?? false + Shares = options.Users ?? [], + OpenAccess = options.Public ?? false }; playlist.SetMediaType(options.MediaType); @@ -171,9 +171,9 @@ namespace Emby.Server.Implementations.Playlists return path; } - private List GetPlaylistItems(IEnumerable itemIds, MediaType playlistMediaType, User user, DtoOptions options) + private IReadOnlyList GetPlaylistItems(IEnumerable itemIds, MediaType playlistMediaType, User user, DtoOptions options) { - var items = itemIds.Select(i => _libraryManager.GetItemById(i)).Where(i => i is not null); + var items = itemIds.Select(_libraryManager.GetItemById).Where(i => i is not null); return Playlist.GetPlaylistItems(playlistMediaType, items, user, options); } @@ -556,11 +556,11 @@ namespace Emby.Server.Implementations.Playlists await UpdatePlaylist(playlist).ConfigureAwait(false); } - public async Task AddToShares(Guid playlistId, Guid userId, Share share) + public async Task AddToShares(Guid playlistId, Guid userId, UserPermissions share) { var playlist = GetPlaylist(userId, playlistId); var shares = playlist.Shares.ToList(); - var existingUserShare = shares.FirstOrDefault(s => s.UserId?.Equals(share.UserId, StringComparison.OrdinalIgnoreCase) ?? false); + var existingUserShare = shares.FirstOrDefault(s => s.UserId.Equals(share.UserId, StringComparison.OrdinalIgnoreCase)); if (existingUserShare is not null) { shares.Remove(existingUserShare); @@ -571,7 +571,7 @@ namespace Emby.Server.Implementations.Playlists await UpdatePlaylist(playlist).ConfigureAwait(false); } - public async Task RemoveFromShares(Guid playlistId, Guid userId, Share share) + public async Task RemoveFromShares(Guid playlistId, Guid userId, UserPermissions share) { var playlist = GetPlaylist(userId, playlistId); var shares = playlist.Shares.ToList(); diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index bf618e8fd7..c38061c7d6 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -93,32 +93,32 @@ public class PlaylistsController : BaseJellyfinApiController ItemIdList = ids, UserId = userId.Value, MediaType = mediaType ?? createPlaylistRequest?.MediaType, - Shares = createPlaylistRequest?.Shares.ToArray(), - OpenAccess = createPlaylistRequest?.OpenAccess + Users = createPlaylistRequest?.Users.ToArray() ?? [], + Public = createPlaylistRequest?.Public }).ConfigureAwait(false); return result; } /// - /// Get a playlist's shares. + /// Get a playlist's users. /// /// The playlist id. /// - /// A list of objects. + /// A list of objects. /// - [HttpGet("{playlistId}/Shares")] + [HttpGet("{playlistId}/User")] [ProducesResponseType(StatusCodes.Status200OK)] - public IReadOnlyList GetPlaylistShares( + public IReadOnlyList GetPlaylistUsers( [FromRoute, Required] Guid playlistId) { var userId = RequestHelpers.GetUserId(User, default); var playlist = _playlistManager.GetPlaylist(userId, playlistId); var isPermitted = playlist.OwnerUserId.Equals(userId) - || playlist.Shares.Any(s => s.CanEdit && (s.UserId?.Equals(userId) ?? false)); + || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(userId)); - return isPermitted ? playlist.Shares : new List(); + return isPermitted ? playlist.Shares : []; } /// @@ -131,14 +131,14 @@ public class PlaylistsController : BaseJellyfinApiController /// [HttpPost("{playlistId}/ToggleOpenAccess")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task ToggleopenAccess( + public async Task ToggleOpenAccess( [FromRoute, Required] Guid playlistId) { var callingUserId = RequestHelpers.GetUserId(User, default); var playlist = _playlistManager.GetPlaylist(callingUserId, playlistId); var isPermitted = playlist.OwnerUserId.Equals(callingUserId) - || playlist.Shares.Any(s => s.CanEdit && (s.UserId?.Equals(callingUserId) ?? false)); + || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)); if (!isPermitted) { @@ -151,35 +151,34 @@ public class PlaylistsController : BaseJellyfinApiController } /// - /// Adds shares to a playlist's shares. + /// Upsert a user to a playlist's users. /// /// The playlist id. - /// The shares. + /// The user id. + /// Edit permission. /// - /// A that represents the asynchronous operation to add shares to a playlist. + /// A that represents the asynchronous operation to upsert an user to a playlist. /// The task result contains an indicating success. /// - [HttpPost("{playlistId}/Shares")] + [HttpPost("{playlistId}/User/{userId}")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task AddUserToPlaylistShares( + public async Task AddUserToPlaylist( [FromRoute, Required] Guid playlistId, - [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Disallow)] Share[] shares) + [FromRoute, Required] Guid userId, + [FromBody] bool canEdit) { var callingUserId = RequestHelpers.GetUserId(User, default); var playlist = _playlistManager.GetPlaylist(callingUserId, playlistId); var isPermitted = playlist.OwnerUserId.Equals(callingUserId) - || playlist.Shares.Any(s => s.CanEdit && (s.UserId?.Equals(callingUserId) ?? false)); + || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)); if (!isPermitted) { return Unauthorized("Unauthorized access"); } - foreach (var share in shares) - { - await _playlistManager.AddToShares(playlistId, callingUserId, share).ConfigureAwait(false); - } + await _playlistManager.AddToShares(playlistId, callingUserId, new UserPermissions(userId.ToString(), canEdit)).ConfigureAwait(false); return NoContent(); } @@ -193,24 +192,24 @@ public class PlaylistsController : BaseJellyfinApiController /// A that represents the asynchronous operation to delete a user from a playlist's shares. /// The task result contains an indicating success. /// - [HttpDelete("{playlistId}/Shares")] + [HttpDelete("{playlistId}/User/{userId}")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task RemoveUserFromPlaylistShares( + public async Task RemoveUserFromPlaylist( [FromRoute, Required] Guid playlistId, - [FromBody] Guid userId) + [FromRoute, Required] Guid userId) { var callingUserId = RequestHelpers.GetUserId(User, default); var playlist = _playlistManager.GetPlaylist(callingUserId, playlistId); var isPermitted = playlist.OwnerUserId.Equals(callingUserId) - || playlist.Shares.Any(s => s.CanEdit && (s.UserId?.Equals(callingUserId) ?? false)); + || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)); if (!isPermitted) { return Unauthorized("Unauthorized access"); } - var share = playlist.Shares.FirstOrDefault(s => s.UserId?.Equals(userId) ?? false); + var share = playlist.Shares.FirstOrDefault(s => s.UserId.Equals(userId)); if (share is null) { diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs index a82bff65ec..6eedd21316 100644 --- a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs +++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs @@ -15,7 +15,7 @@ public class CreatePlaylistDto /// /// Gets or sets the name of the new playlist. /// - public string? Name { get; set; } + public required string Name { get; set; } /// /// Gets or sets item ids to add to the playlist. @@ -34,12 +34,12 @@ public class CreatePlaylistDto public MediaType? MediaType { get; set; } /// - /// Gets or sets the shares. + /// Gets or sets the playlist users. /// - public IReadOnlyList Shares { get; set; } = []; + public IReadOnlyList Users { get; set; } = []; /// - /// Gets or sets a value indicating whether open access is enabled. + /// Gets or sets a value indicating whether the playlist is public. /// - public bool OpenAccess { get; set; } + public bool Public { get; set; } = true; } diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index aaca1cc492..238923d296 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -41,7 +41,7 @@ namespace MediaBrowser.Controller.Playlists /// The user identifier. /// The share. /// Task. - Task AddToShares(Guid playlistId, Guid userId, Share share); + Task AddToShares(Guid playlistId, Guid userId, UserPermissions share); /// /// Rremoves a share from the playlist. @@ -50,7 +50,7 @@ namespace MediaBrowser.Controller.Playlists /// The user identifier. /// The share. /// Task. - Task RemoveFromShares(Guid playlistId, Guid userId, Share share); + Task RemoveFromShares(Guid playlistId, Guid userId, UserPermissions share); /// /// Creates the playlist. diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index 9a08a4ce3e..dfd9b83302 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -32,7 +32,7 @@ namespace MediaBrowser.Controller.Playlists public Playlist() { - Shares = Array.Empty(); + Shares = []; OpenAccess = false; } @@ -40,7 +40,7 @@ namespace MediaBrowser.Controller.Playlists public bool OpenAccess { get; set; } - public IReadOnlyList Shares { get; set; } + public IReadOnlyList Shares { get; set; } [JsonIgnore] public bool IsFile => IsPlaylistFile(Path); @@ -129,7 +129,7 @@ namespace MediaBrowser.Controller.Playlists protected override List LoadChildren() { // Save a trip to the database - return new List(); + return []; } protected override Task ValidateChildrenInternal(IProgress progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) @@ -144,7 +144,7 @@ namespace MediaBrowser.Controller.Playlists protected override IEnumerable GetNonCachedChildren(IDirectoryService directoryService) { - return new List(); + return []; } public override IEnumerable GetRecursiveChildren(User user, InternalItemsQuery query) @@ -166,7 +166,7 @@ namespace MediaBrowser.Controller.Playlists return base.GetChildren(user, true, query); } - public static List GetPlaylistItems(MediaType playlistMediaType, IEnumerable inputItems, User user, DtoOptions options) + public static IReadOnlyList GetPlaylistItems(MediaType playlistMediaType, IEnumerable inputItems, User user, DtoOptions options) { if (user is not null) { diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index 8a870e0d9b..4ee1b2ef69 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -519,7 +519,7 @@ namespace MediaBrowser.LocalMetadata.Parsers private void FetchFromSharesNode(XmlReader reader, IHasShares item) { - var list = new List(); + var list = new List(); reader.MoveToContent(); reader.Read(); @@ -565,7 +565,7 @@ namespace MediaBrowser.LocalMetadata.Parsers } } - item.Shares = list.ToArray(); + item.Shares = [.. list]; } /// @@ -830,12 +830,12 @@ namespace MediaBrowser.LocalMetadata.Parsers /// /// The xml reader. /// The share. - protected Share? GetShare(XmlReader reader) + protected UserPermissions? GetShare(XmlReader reader) { - var item = new Share(); - reader.MoveToContent(); reader.Read(); + string? userId = null; + var canEdit = false; // Loop through each element while (!reader.EOF && reader.ReadState == ReadState.Interactive) @@ -845,10 +845,10 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "UserId": - item.UserId = reader.ReadNormalizedString(); + userId = reader.ReadNormalizedString(); break; case "CanEdit": - item.CanEdit = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase); + canEdit = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase); break; default: reader.Skip(); @@ -862,9 +862,9 @@ namespace MediaBrowser.LocalMetadata.Parsers } // This is valid - if (!string.IsNullOrWhiteSpace(item.UserId)) + if (!string.IsNullOrWhiteSpace(userId)) { - return item; + return new UserPermissions(userId, canEdit); } return null; diff --git a/MediaBrowser.Model/Entities/IHasShares.cs b/MediaBrowser.Model/Entities/IHasShares.cs index 31574a3ffa..fb6b6e424b 100644 --- a/MediaBrowser.Model/Entities/IHasShares.cs +++ b/MediaBrowser.Model/Entities/IHasShares.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace MediaBrowser.Model.Entities; @@ -10,5 +10,5 @@ public interface IHasShares /// /// Gets or sets the shares. /// - IReadOnlyList Shares { get; set; } + IReadOnlyList Shares { get; set; } } diff --git a/MediaBrowser.Model/Entities/Share.cs b/MediaBrowser.Model/Entities/Share.cs deleted file mode 100644 index 186aad1892..0000000000 --- a/MediaBrowser.Model/Entities/Share.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace MediaBrowser.Model.Entities; - -/// -/// Class to hold data on sharing permissions. -/// -public class Share -{ - /// - /// Gets or sets the user id. - /// - public string? UserId { get; set; } - - /// - /// Gets or sets a value indicating whether the user has edit permissions. - /// - public bool CanEdit { get; set; } -} diff --git a/MediaBrowser.Model/Entities/UserPermissions.cs b/MediaBrowser.Model/Entities/UserPermissions.cs new file mode 100644 index 0000000000..271feed114 --- /dev/null +++ b/MediaBrowser.Model/Entities/UserPermissions.cs @@ -0,0 +1,19 @@ +namespace MediaBrowser.Model.Entities; + +/// +/// Class to hold data on user permissions for lists. +/// +/// The user id. +/// Edit permission. +public class UserPermissions(string userId, bool canEdit = false) +{ + /// + /// Gets or sets the user id. + /// + public string UserId { get; set; } = userId; + + /// + /// Gets or sets a value indicating whether the user has edit permissions. + /// + public bool CanEdit { get; set; } = canEdit; +} diff --git a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs index 93eccd5c77..f1351588fb 100644 --- a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs +++ b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs @@ -18,7 +18,7 @@ public class PlaylistCreationRequest /// /// Gets or sets the list of items. /// - public IReadOnlyList ItemIdList { get; set; } = Array.Empty(); + public IReadOnlyList ItemIdList { get; set; } = []; /// /// Gets or sets the media type. @@ -31,12 +31,12 @@ public class PlaylistCreationRequest public Guid UserId { get; set; } /// - /// Gets or sets the shares. + /// Gets or sets the user permissions. /// - public IReadOnlyList? Shares { get; set; } + public IReadOnlyList Users { get; set; } = []; /// - /// Gets or sets a value indicating whether open access is enabled. + /// Gets or sets a value indicating whether the playlist is public. /// - public bool? OpenAccess { get; set; } + public bool? Public { get; set; } = true; } From 2aaa9f669a0ba6855329ce63ae4e785dc70a5a69 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 27 Mar 2024 06:39:14 +0100 Subject: [PATCH 005/444] Apply review suggestions --- .../Controllers/PlaylistsController.cs | 73 ++++++++++++++----- .../Entities/UserPermissions.cs | 19 +++-- 2 files changed, 69 insertions(+), 23 deletions(-) diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index c38061c7d6..7ca04d7ba5 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -104,39 +104,58 @@ public class PlaylistsController : BaseJellyfinApiController /// Get a playlist's users. /// /// The playlist id. + /// Found shares. + /// Unauthorized access. + /// Playlist not found. /// /// A list of objects. /// [HttpGet("{playlistId}/User")] [ProducesResponseType(StatusCodes.Status200OK)] - public IReadOnlyList GetPlaylistUsers( + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult> GetPlaylistUsers( [FromRoute, Required] Guid playlistId) { - var userId = RequestHelpers.GetUserId(User, default); + var userId = User.GetUserId(); var playlist = _playlistManager.GetPlaylist(userId, playlistId); + if (playlist is null) + { + return NotFound("Playlist not found"); + } + var isPermitted = playlist.OwnerUserId.Equals(userId) || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(userId)); - return isPermitted ? playlist.Shares : []; + return isPermitted ? playlist.Shares.ToList() : Unauthorized("Unauthorized Access"); } /// - /// Toggles OpenAccess of a playlist. + /// Toggles public access of a playlist. /// /// The playlist id. + /// Public access toggled. + /// Unauthorized access. + /// Playlist not found. /// - /// A that represents the asynchronous operation to toggle OpenAccess of a playlist. + /// A that represents the asynchronous operation to toggle public access of a playlist. /// The task result contains an indicating success. /// - [HttpPost("{playlistId}/ToggleOpenAccess")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task ToggleOpenAccess( + [HttpPost("{playlistId}/TogglePublic")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task TogglePublicAccess( [FromRoute, Required] Guid playlistId) { - var callingUserId = RequestHelpers.GetUserId(User, default); + var callingUserId = User.GetUserId(); var playlist = _playlistManager.GetPlaylist(callingUserId, playlistId); + if (playlist is null) + { + return NotFound("Playlist not found"); + } + var isPermitted = playlist.OwnerUserId.Equals(callingUserId) || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)); @@ -151,25 +170,34 @@ public class PlaylistsController : BaseJellyfinApiController } /// - /// Upsert a user to a playlist's users. + /// Modify a user to a playlist's users. /// /// The playlist id. /// The user id. /// Edit permission. + /// User's permissions modified. + /// Unauthorized access. + /// Playlist not found. /// - /// A that represents the asynchronous operation to upsert an user to a playlist. + /// A that represents the asynchronous operation to modify an user's playlist permissions. /// The task result contains an indicating success. /// [HttpPost("{playlistId}/User/{userId}")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task AddUserToPlaylist( + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task ModifyPlaylistUserPermissions( [FromRoute, Required] Guid playlistId, [FromRoute, Required] Guid userId, [FromBody] bool canEdit) { - var callingUserId = RequestHelpers.GetUserId(User, default); + var callingUserId = User.GetUserId(); var playlist = _playlistManager.GetPlaylist(callingUserId, playlistId); + if (playlist is null) + { + return NotFound("Playlist not found"); + } + var isPermitted = playlist.OwnerUserId.Equals(callingUserId) || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)); @@ -188,19 +216,29 @@ public class PlaylistsController : BaseJellyfinApiController /// /// The playlist id. /// The user id. + /// User permissions removed from playlist. + /// Unauthorized access. + /// No playlist or user permissions found. /// /// A that represents the asynchronous operation to delete a user from a playlist's shares. /// The task result contains an indicating success. /// [HttpDelete("{playlistId}/User/{userId}")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task RemoveUserFromPlaylist( [FromRoute, Required] Guid playlistId, [FromRoute, Required] Guid userId) { - var callingUserId = RequestHelpers.GetUserId(User, default); + var callingUserId = User.GetUserId(); var playlist = _playlistManager.GetPlaylist(callingUserId, playlistId); + if (playlist is null) + { + return NotFound("Playlist not found"); + } + var isPermitted = playlist.OwnerUserId.Equals(callingUserId) || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)); @@ -210,10 +248,9 @@ public class PlaylistsController : BaseJellyfinApiController } var share = playlist.Shares.FirstOrDefault(s => s.UserId.Equals(userId)); - if (share is null) { - return NotFound(); + return NotFound("User permissions not found"); } await _playlistManager.RemoveFromShares(playlistId, callingUserId, share).ConfigureAwait(false); diff --git a/MediaBrowser.Model/Entities/UserPermissions.cs b/MediaBrowser.Model/Entities/UserPermissions.cs index 271feed114..80e2cd32c2 100644 --- a/MediaBrowser.Model/Entities/UserPermissions.cs +++ b/MediaBrowser.Model/Entities/UserPermissions.cs @@ -3,17 +3,26 @@ namespace MediaBrowser.Model.Entities; /// /// Class to hold data on user permissions for lists. /// -/// The user id. -/// Edit permission. -public class UserPermissions(string userId, bool canEdit = false) +public class UserPermissions { + /// + /// Initializes a new instance of the class. + /// + /// The user id. + /// Edit permission. + public UserPermissions(string userId, bool canEdit = false) + { + UserId = userId; + CanEdit = canEdit; + } + /// /// Gets or sets the user id. /// - public string UserId { get; set; } = userId; + public string UserId { get; set; } /// /// Gets or sets a value indicating whether the user has edit permissions. /// - public bool CanEdit { get; set; } = canEdit; + public bool CanEdit { get; set; } } From a8f1668540be34bc118c9bf08a234b856081011f Mon Sep 17 00:00:00 2001 From: gnattu Date: Sat, 30 Mar 2024 22:53:46 +0800 Subject: [PATCH 006/444] fix: unset qmin and qmax for vt (#11246) Co-authored-by: Nyanmisaka --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 85963e66c3..ce5e298142 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1333,7 +1333,7 @@ namespace MediaBrowser.Controller.MediaEncoding return ".ts"; } - public string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec) + private string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec) { if (state.OutputVideoBitrate is null) { @@ -1402,7 +1402,7 @@ namespace MediaBrowser.Controller.MediaEncoding { // The `maxrate` and `bufsize` options can potentially lead to performance regression // and even encoder hangs, especially when the value is very high. - return FormattableString.Invariant($" -b:v {bitrate}"); + return FormattableString.Invariant($" -b:v {bitrate} -qmin -1 -qmax -1"); } return FormattableString.Invariant($" -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}"); From 915df8771603349be55a28b6e22a407f4927d625 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sat, 30 Mar 2024 16:24:21 +0100 Subject: [PATCH 007/444] Support "extra" folder for extras content (#11249) --- Emby.Naming/Common/NamingOptions.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 4bd226d95e..333d237a24 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -537,6 +537,12 @@ namespace Emby.Naming.Common "extras", MediaType.Video), + new ExtraRule( + ExtraType.Unknown, + ExtraRuleType.DirectoryName, + "extra", + MediaType.Video), + new ExtraRule( ExtraType.Unknown, ExtraRuleType.DirectoryName, From 7cfe0009e5a528400e41e18bed7bba7ed80a9bb3 Mon Sep 17 00:00:00 2001 From: Mikal S <7761729+revam@users.noreply.github.com> Date: Sat, 30 Mar 2024 16:24:28 +0100 Subject: [PATCH 008/444] fix: add image count check to splash screen generation (#11245) --- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index a158e5c866..4f6ed4469f 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -554,9 +554,13 @@ public class SkiaEncoder : IImageEncoder /// public void CreateSplashscreen(IReadOnlyList posters, IReadOnlyList backdrops) { - var splashBuilder = new SplashscreenBuilder(this); - var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png"); - splashBuilder.GenerateSplash(posters, backdrops, outputPath); + // Only generate the splash screen if we have at least one poster and at least one backdrop/thumbnail. + if (posters.Count > 0 && backdrops.Count > 0) + { + var splashBuilder = new SplashscreenBuilder(this); + var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png"); + splashBuilder.GenerateSplash(posters, backdrops, outputPath); + } } /// From fe88a484d18ddbb2fb6859b93ff2e05a166359f1 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sat, 30 Mar 2024 23:25:22 +0800 Subject: [PATCH 009/444] fix: don't do empty hwupload for VT (#11235) --- .../MediaEncoding/EncodingHelper.cs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index ce5e298142..250e0143f9 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -5119,11 +5119,6 @@ namespace MediaBrowser.Controller.MediaEncoding /* Make main filters for video stream */ var mainFilters = new List(); - // INPUT videotoolbox/memory surface(vram/uma) - // this will pass-through automatically if in/out format matches. - mainFilters.Add("format=nv12|p010le|videotoolbox_vld"); - mainFilters.Add("hwupload=derive_device=videotoolbox"); - // hw deint if (doDeintH2645) { @@ -5179,6 +5174,21 @@ namespace MediaBrowser.Controller.MediaEncoding overlayFilters.Add("overlay_videotoolbox=eof_action=pass:repeatlast=0"); } + var needFiltering = mainFilters.Any(f => !string.IsNullOrEmpty(f)) || + subFilters.Any(f => !string.IsNullOrEmpty(f)) || + overlayFilters.Any(f => !string.IsNullOrEmpty(f)); + + // This is a workaround for ffmpeg's hwupload implementation + // For VideoToolbox encoders, a hwupload without a valid filter actually consuming its frame + // will cause the encoder to produce incorrect frames. + if (needFiltering) + { + // INPUT videotoolbox/memory surface(vram/uma) + // this will pass-through automatically if in/out format matches. + mainFilters.Insert(0, "format=nv12|p010le|videotoolbox_vld"); + mainFilters.Insert(0, "hwupload=derive_device=videotoolbox"); + } + return (mainFilters, subFilters, overlayFilters); } From 4201079b349c34372aa9375791aa86d7e90572f1 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sat, 30 Mar 2024 17:30:00 +0100 Subject: [PATCH 010/444] fix: use a reentrant lock when accessing active connections (#11256) --- .../HttpServer/WebSocketManager.cs | 22 ++++++----- .../Net/BasePeriodicWebSocketListener.cs | 37 +++---------------- 2 files changed, 18 insertions(+), 41 deletions(-) diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs index 52f14b0b10..774d3563cb 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs @@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.HttpServer WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); - using var connection = new WebSocketConnection( + var connection = new WebSocketConnection( _loggerFactory.CreateLogger(), webSocket, authorizationInfo, @@ -56,17 +56,19 @@ namespace Emby.Server.Implementations.HttpServer { OnReceive = ProcessWebSocketMessageReceived }; - - var tasks = new Task[_webSocketListeners.Length]; - for (var i = 0; i < _webSocketListeners.Length; ++i) + await using (connection.ConfigureAwait(false)) { - tasks[i] = _webSocketListeners[i].ProcessWebSocketConnectedAsync(connection, context); + var tasks = new Task[_webSocketListeners.Length]; + for (var i = 0; i < _webSocketListeners.Length; ++i) + { + tasks[i] = _webSocketListeners[i].ProcessWebSocketConnectedAsync(connection, context); + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + + await connection.ReceiveAsync().ConfigureAwait(false); + _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress); } - - await Task.WhenAll(tasks).ConfigureAwait(false); - - await connection.ReceiveAsync().ConfigureAwait(false); - _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress); } catch (Exception ex) // Otherwise ASP.Net will ignore the exception { diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index 219da309e4..06386f2b86 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -33,7 +33,7 @@ namespace MediaBrowser.Controller.Net SingleWriter = false }); - private readonly SemaphoreSlim _lock = new(1, 1); + private readonly object _activeConnectionsLock = new(); /// /// The _active connections. @@ -126,15 +126,10 @@ namespace MediaBrowser.Controller.Net InitialDelayMs = dueTimeMs }; - _lock.Wait(); - try + lock (_activeConnectionsLock) { _activeConnections.Add((message.Connection, cancellationTokenSource, state)); } - finally - { - _lock.Release(); - } } protected void SendData(bool force) @@ -153,8 +148,7 @@ namespace MediaBrowser.Controller.Net (IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State)[] tuples; var now = DateTime.UtcNow; - await _lock.WaitAsync().ConfigureAwait(false); - try + lock (_activeConnectionsLock) { if (_activeConnections.Count == 0) { @@ -174,10 +168,6 @@ namespace MediaBrowser.Controller.Net }) .ToArray(); } - finally - { - _lock.Release(); - } if (tuples.Length == 0) { @@ -240,8 +230,7 @@ namespace MediaBrowser.Controller.Net /// The message. private void Stop(WebSocketMessageInfo message) { - _lock.Wait(); - try + lock (_activeConnectionsLock) { var connection = _activeConnections.FirstOrDefault(c => c.Connection == message.Connection); @@ -250,10 +239,6 @@ namespace MediaBrowser.Controller.Net DisposeConnection(connection); } } - finally - { - _lock.Release(); - } } /// @@ -283,15 +268,10 @@ namespace MediaBrowser.Controller.Net Logger.LogError(ex, "Error disposing websocket"); } - _lock.Wait(); - try + lock (_activeConnectionsLock) { _activeConnections.Remove(connection); } - finally - { - _lock.Release(); - } } protected virtual async ValueTask DisposeAsyncCore() @@ -306,18 +286,13 @@ namespace MediaBrowser.Controller.Net Logger.LogError(ex, "Disposing the message consumer failed"); } - await _lock.WaitAsync().ConfigureAwait(false); - try + lock (_activeConnectionsLock) { foreach (var connection in _activeConnections.ToArray()) { DisposeConnection(connection); } } - finally - { - _lock.Release(); - } } /// From bfc5deb234ea89fe95482f30c02efad544229daf Mon Sep 17 00:00:00 2001 From: Sebastian Held Date: Sat, 30 Mar 2024 17:40:27 +0100 Subject: [PATCH 011/444] fix metadata refresh for artists (#11257) --- MediaBrowser.Providers/Manager/ProviderManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index f340349641..a9ebf7ec72 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -1106,7 +1106,8 @@ namespace MediaBrowser.Providers.Manager var musicArtists = albums .Select(i => i.MusicArtist) - .Where(i => i is not null); + .Where(i => i is not null) + .Distinct(); var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new Progress(), options, true, cancellationToken)); From 000395e03682ff796de56c76be09575f07853be1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 30 Mar 2024 17:27:51 -0600 Subject: [PATCH 012/444] chore(deps): update dependency efcoresecondlevelcacheinterceptor to v4.3.0 (#11263) 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 ac0a523c58..b6bf5f3e4b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,7 +16,7 @@ - + From ed82d796473455c3b89e640802eee24242801a3b Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Sat, 30 Mar 2024 17:28:03 -0600 Subject: [PATCH 013/444] Catch exceptions in auto discovery (#11252) --- src/Jellyfin.Networking/AutoDiscoveryHost.cs | 42 ++++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/Jellyfin.Networking/AutoDiscoveryHost.cs b/src/Jellyfin.Networking/AutoDiscoveryHost.cs index 5624c4ed13..2be57d7a1e 100644 --- a/src/Jellyfin.Networking/AutoDiscoveryHost.cs +++ b/src/Jellyfin.Networking/AutoDiscoveryHost.cs @@ -78,28 +78,36 @@ public sealed class AutoDiscoveryHost : BackgroundService private async Task ListenForAutoDiscoveryMessage(IPAddress address, CancellationToken cancellationToken) { - using var udpClient = new UdpClient(new IPEndPoint(address, PortNumber)); - udpClient.MulticastLoopback = false; - - while (!cancellationToken.IsCancellationRequested) + try { - try + using var udpClient = new UdpClient(new IPEndPoint(address, PortNumber)); + udpClient.MulticastLoopback = false; + + while (!cancellationToken.IsCancellationRequested) { - var result = await udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false); - var text = Encoding.UTF8.GetString(result.Buffer); - if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase)) + try { - await RespondToV2Message(udpClient, result.RemoteEndPoint, cancellationToken).ConfigureAwait(false); + var result = await udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false); + var text = Encoding.UTF8.GetString(result.Buffer); + if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase)) + { + await RespondToV2Message(udpClient, result.RemoteEndPoint, cancellationToken).ConfigureAwait(false); + } + } + catch (SocketException ex) + { + _logger.LogError(ex, "Failed to receive data from socket"); } } - catch (SocketException ex) - { - _logger.LogError(ex, "Failed to receive data from socket"); - } - catch (OperationCanceledException) - { - _logger.LogDebug("Broadcast socket operation cancelled"); - } + } + catch (OperationCanceledException) + { + _logger.LogDebug("Broadcast socket operation cancelled"); + } + catch (Exception ex) + { + // Exception in this function will prevent the background service from restarting in-process. + _logger.LogError(ex, "Unable to bind to {Address}:{Port}", address, PortNumber); } } From 904c3873fe60c26f68d0b17eaf1d5e890bb6d9ee Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sun, 31 Mar 2024 22:45:59 +0200 Subject: [PATCH 014/444] Remove SessionInfo.FullNowPlayingItem from API responses (#11268) --- MediaBrowser.Controller/Session/SessionInfo.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index 3a12a56f1e..76d5d3a3f2 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -134,6 +134,7 @@ namespace MediaBrowser.Controller.Session /// The now playing item. public BaseItemDto NowPlayingItem { get; set; } + [JsonIgnore] public BaseItem FullNowPlayingItem { get; set; } public BaseItemDto NowViewingItem { get; set; } From a45f2936e1912d9df7144e150833cd5853a04c09 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 31 Mar 2024 14:46:18 -0600 Subject: [PATCH 015/444] chore(deps): update dependency efcoresecondlevelcacheinterceptor to v4.3.1 (#11267) 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 b6bf5f3e4b..308d40f33f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,7 +16,7 @@ - + From 84b933d8355c718d3674f3b7371a190849071970 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sun, 31 Mar 2024 22:48:46 +0200 Subject: [PATCH 016/444] Use enum for BaseItemDto.ExtraType (#11261) --- Emby.Server.Implementations/Dto/DtoService.cs | 10 ++-------- MediaBrowser.Model/Dto/BaseItemDto.cs | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 7812687ea3..5da9bea262 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -903,10 +903,7 @@ namespace Emby.Server.Implementations.Dto if (item is Audio audio) { dto.Album = audio.Album; - if (audio.ExtraType.HasValue) - { - dto.ExtraType = audio.ExtraType.Value.ToString(); - } + dto.ExtraType = audio.ExtraType; var albumParent = audio.AlbumEntity; @@ -1058,10 +1055,7 @@ namespace Emby.Server.Implementations.Dto dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult(); } - if (video.ExtraType.HasValue) - { - dto.ExtraType = video.ExtraType.Value.ToString(); - } + dto.ExtraType = video.ExtraType; } if (options.ContainsField(ItemFields.MediaStreams)) diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index cfff717db2..6d5c84e1de 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -65,7 +65,7 @@ namespace MediaBrowser.Model.Dto public DateTime? DateLastMediaAdded { get; set; } - public string ExtraType { get; set; } + public ExtraType? ExtraType { get; set; } public int? AirsBeforeSeasonNumber { get; set; } From d9fe900952db446ded5ebdb937bd9e242b4a96de Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sun, 31 Mar 2024 22:48:56 +0200 Subject: [PATCH 017/444] Fix FindExtras overwriting current extra type (#11260) --- Emby.Server.Implementations/Library/LibraryManager.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index a2abafd2ae..0c854bdb74 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2677,7 +2677,12 @@ namespace Emby.Server.Implementations.Library extra = itemById; } - extra.ExtraType = extraType; + // Only update extra type if it is more specific then the currently known extra type + if (extra.ExtraType is null or ExtraType.Unknown || extraType != ExtraType.Unknown) + { + extra.ExtraType = extraType; + } + extra.ParentId = Guid.Empty; extra.OwnerId = owner.Id; return extra; From 3ade3a8e63204becb856bbc268ce877c82a44a2c Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Sun, 31 Mar 2024 21:58:06 -0600 Subject: [PATCH 018/444] Lowercase CollectionTypeOptions to match legacy experience (#11272) --- .../Collections/CollectionManager.cs | 2 +- .../Entities/CollectionTypeOptions.cs | 59 +++++++++++---- .../Entities/VirtualFolderInfo.cs | 1 - .../Json/Converters/JsonLowerCaseConverter.cs | 25 ------- .../Recordings/RecordingsManager.cs | 4 +- .../Converters/JsonLowerCaseConverterTests.cs | 71 ------------------- 6 files changed, 49 insertions(+), 113 deletions(-) delete mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonLowerCaseConverter.cs delete mode 100644 tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index b34d0f21ef..e414792ba0 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -102,7 +102,7 @@ namespace Emby.Server.Implementations.Collections var name = _localizationManager.GetLocalizedString("Collections"); - await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.BoxSets, libraryOptions, true).ConfigureAwait(false); + await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.boxsets, libraryOptions, true).ConfigureAwait(false); return FindFolders(path).First(); } diff --git a/MediaBrowser.Model/Entities/CollectionTypeOptions.cs b/MediaBrowser.Model/Entities/CollectionTypeOptions.cs index e1894d84ad..fc4cfdd66c 100644 --- a/MediaBrowser.Model/Entities/CollectionTypeOptions.cs +++ b/MediaBrowser.Model/Entities/CollectionTypeOptions.cs @@ -1,16 +1,49 @@ -#pragma warning disable CS1591 +#pragma warning disable SA1300 // Lowercase required for backwards compat. -namespace MediaBrowser.Model.Entities +namespace MediaBrowser.Model.Entities; + +/// +/// The collection type options. +/// +public enum CollectionTypeOptions { - public enum CollectionTypeOptions - { - Movies = 0, - TvShows = 1, - Music = 2, - MusicVideos = 3, - HomeVideos = 4, - BoxSets = 5, - Books = 6, - Mixed = 7 - } + /// + /// Movies. + /// + movies = 0, + + /// + /// TV Shows. + /// + tvshows = 1, + + /// + /// Music. + /// + music = 2, + + /// + /// Music Videos. + /// + musicvideos = 3, + + /// + /// Home Videos (and Photos). + /// + homevideos = 4, + + /// + /// Box Sets. + /// + boxsets = 5, + + /// + /// Books. + /// + books = 6, + + /// + /// Mixed Movies and TV Shows. + /// + mixed = 7 } diff --git a/MediaBrowser.Model/Entities/VirtualFolderInfo.cs b/MediaBrowser.Model/Entities/VirtualFolderInfo.cs index 2b2bda12c3..89bb72c3c8 100644 --- a/MediaBrowser.Model/Entities/VirtualFolderInfo.cs +++ b/MediaBrowser.Model/Entities/VirtualFolderInfo.cs @@ -37,7 +37,6 @@ namespace MediaBrowser.Model.Entities /// Gets or sets the type of the collection. /// /// The type of the collection. - [JsonConverter(typeof(JsonLowerCaseConverter))] public CollectionTypeOptions? CollectionType { get; set; } public LibraryOptions LibraryOptions { get; set; } diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonLowerCaseConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonLowerCaseConverter.cs deleted file mode 100644 index cd582ced64..0000000000 --- a/src/Jellyfin.Extensions/Json/Converters/JsonLowerCaseConverter.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Jellyfin.Extensions.Json.Converters -{ - /// - /// Converts an object to a lowercase string. - /// - /// The object type. - public class JsonLowerCaseConverter : JsonConverter - { - /// - public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return JsonSerializer.Deserialize(ref reader, options); - } - - /// - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value?.ToString()?.ToLowerInvariant()); - } - } -} diff --git a/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs b/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs index 92605a1eb9..2f4caa3867 100644 --- a/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs +++ b/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs @@ -159,7 +159,7 @@ public sealed class RecordingsManager : IRecordingsManager, IDisposable { Locations = [customPath], Name = "Recorded Movies", - CollectionType = CollectionTypeOptions.Movies + CollectionType = CollectionTypeOptions.movies }; } @@ -172,7 +172,7 @@ public sealed class RecordingsManager : IRecordingsManager, IDisposable { Locations = [customPath], Name = "Recorded Shows", - CollectionType = CollectionTypeOptions.TvShows + CollectionType = CollectionTypeOptions.tvshows }; } } diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs deleted file mode 100644 index 16c69ca489..0000000000 --- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using Jellyfin.Extensions.Json.Converters; -using MediaBrowser.Model.Entities; -using Xunit; - -namespace Jellyfin.Extensions.Tests.Json.Converters -{ - public class JsonLowerCaseConverterTests - { - private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions() - { - Converters = - { - new JsonStringEnumConverter() - } - }; - - [Theory] - [InlineData(null, "{\"CollectionType\":null}")] - [InlineData(CollectionTypeOptions.Movies, "{\"CollectionType\":\"movies\"}")] - [InlineData(CollectionTypeOptions.MusicVideos, "{\"CollectionType\":\"musicvideos\"}")] - public void Serialize_CollectionTypeOptions_Correct(CollectionTypeOptions? collectionType, string expected) - { - Assert.Equal(expected, JsonSerializer.Serialize(new TestContainer(collectionType), _jsonOptions)); - } - - [Theory] - [InlineData("{\"CollectionType\":null}", null)] - [InlineData("{\"CollectionType\":\"movies\"}", CollectionTypeOptions.Movies)] - [InlineData("{\"CollectionType\":\"musicvideos\"}", CollectionTypeOptions.MusicVideos)] - public void Deserialize_CollectionTypeOptions_Correct(string json, CollectionTypeOptions? result) - { - var res = JsonSerializer.Deserialize(json, _jsonOptions); - Assert.NotNull(res); - Assert.Equal(result, res!.CollectionType); - } - - [Theory] - [InlineData(null)] - [InlineData(CollectionTypeOptions.Movies)] - [InlineData(CollectionTypeOptions.MusicVideos)] - public void RoundTrip_CollectionTypeOptions_Correct(CollectionTypeOptions? value) - { - var res = JsonSerializer.Deserialize(JsonSerializer.Serialize(new TestContainer(value), _jsonOptions), _jsonOptions); - Assert.NotNull(res); - Assert.Equal(value, res!.CollectionType); - } - - [Theory] - [InlineData("{\"CollectionType\":null}")] - [InlineData("{\"CollectionType\":\"movies\"}")] - [InlineData("{\"CollectionType\":\"musicvideos\"}")] - public void RoundTrip_String_Correct(string json) - { - var res = JsonSerializer.Serialize(JsonSerializer.Deserialize(json, _jsonOptions), _jsonOptions); - Assert.Equal(json, res); - } - - private sealed class TestContainer - { - public TestContainer(CollectionTypeOptions? collectionType) - { - CollectionType = collectionType; - } - - [JsonConverter(typeof(JsonLowerCaseConverter))] - public CollectionTypeOptions? CollectionType { get; set; } - } - } -} From e12c666f709a25de0a8e3ab0487463158fdbb202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Dav=C3=B3?= Date: Mon, 1 Apr 2024 11:43:48 +0000 Subject: [PATCH 019/444] 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 fe10be3085..91e29d9266 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -30,7 +30,7 @@ "ItemAddedWithName": "{0} se ha añadido a la biblioteca", "ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca", "LabelIpAddressValue": "Dirección IP: {0}", - "LabelRunningTimeValue": "Tiempo de funcionamiento: {0}", + "LabelRunningTimeValue": "Duración: {0}", "Latest": "Últimas", "MessageApplicationUpdated": "Se ha actualizado el servidor Jellyfin", "MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}", From bff37ed13aa9ee0267ee5e1248339c6044fa1b0c Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Mon, 1 Apr 2024 19:59:48 +0200 Subject: [PATCH 020/444] Apply review suggestions --- .../Playlists/PlaylistManager.cs | 10 +++++----- Jellyfin.Api/Controllers/PlaylistsController.cs | 6 +++--- .../Models/PlaylistDtos/CreatePlaylistDto.cs | 2 +- .../Playlists/IPlaylistManager.cs | 4 ++-- MediaBrowser.Controller/Playlists/Playlist.cs | 2 +- .../Parsers/BaseItemXmlParser.cs | 6 +++--- MediaBrowser.Model/Entities/IHasShares.cs | 2 +- ...UserPermissions.cs => PlaylistUserPermissions.cs} | 12 +++++++----- .../Playlists/PlaylistCreationRequest.cs | 2 +- 9 files changed, 24 insertions(+), 22 deletions(-) rename MediaBrowser.Model/Entities/{UserPermissions.cs => PlaylistUserPermissions.cs} (62%) diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 59c96852af..0e5add635d 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -526,9 +526,9 @@ namespace Emby.Server.Implementations.Playlists { // Update owner if shared var rankedShares = playlist.Shares.OrderByDescending(x => x.CanEdit).ToArray(); - if (rankedShares.Length > 0 && Guid.TryParse(rankedShares[0].UserId, out var guid)) + if (rankedShares.Length > 0) { - playlist.OwnerUserId = guid; + playlist.OwnerUserId = rankedShares[0].UserId; playlist.Shares = rankedShares.Skip(1).ToArray(); await UpdatePlaylist(playlist).ConfigureAwait(false); } @@ -556,11 +556,11 @@ namespace Emby.Server.Implementations.Playlists await UpdatePlaylist(playlist).ConfigureAwait(false); } - public async Task AddToShares(Guid playlistId, Guid userId, UserPermissions share) + public async Task AddToShares(Guid playlistId, Guid userId, PlaylistUserPermissions share) { var playlist = GetPlaylist(userId, playlistId); var shares = playlist.Shares.ToList(); - var existingUserShare = shares.FirstOrDefault(s => s.UserId.Equals(share.UserId, StringComparison.OrdinalIgnoreCase)); + var existingUserShare = shares.FirstOrDefault(s => s.UserId.Equals(share.UserId)); if (existingUserShare is not null) { shares.Remove(existingUserShare); @@ -571,7 +571,7 @@ namespace Emby.Server.Implementations.Playlists await UpdatePlaylist(playlist).ConfigureAwait(false); } - public async Task RemoveFromShares(Guid playlistId, Guid userId, UserPermissions share) + public async Task RemoveFromShares(Guid playlistId, Guid userId, PlaylistUserPermissions share) { var playlist = GetPlaylist(userId, playlistId); var shares = playlist.Shares.ToList(); diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 7ca04d7ba5..862e5235ee 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -108,13 +108,13 @@ public class PlaylistsController : BaseJellyfinApiController /// Unauthorized access. /// Playlist not found. /// - /// A list of objects. + /// A list of objects. /// [HttpGet("{playlistId}/User")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult> GetPlaylistUsers( + public ActionResult> GetPlaylistUsers( [FromRoute, Required] Guid playlistId) { var userId = User.GetUserId(); @@ -206,7 +206,7 @@ public class PlaylistsController : BaseJellyfinApiController return Unauthorized("Unauthorized access"); } - await _playlistManager.AddToShares(playlistId, callingUserId, new UserPermissions(userId.ToString(), canEdit)).ConfigureAwait(false); + await _playlistManager.AddToShares(playlistId, callingUserId, new PlaylistUserPermissions(userId.ToString(), canEdit)).ConfigureAwait(false); return NoContent(); } diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs index 6eedd21316..69694a7699 100644 --- a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs +++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs @@ -36,7 +36,7 @@ public class CreatePlaylistDto /// /// Gets or sets the playlist users. /// - public IReadOnlyList Users { get; set; } = []; + public IReadOnlyList Users { get; set; } = []; /// /// Gets or sets a value indicating whether the playlist is public. diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index 238923d296..1750be6199 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -41,7 +41,7 @@ namespace MediaBrowser.Controller.Playlists /// The user identifier. /// The share. /// Task. - Task AddToShares(Guid playlistId, Guid userId, UserPermissions share); + Task AddToShares(Guid playlistId, Guid userId, PlaylistUserPermissions share); /// /// Rremoves a share from the playlist. @@ -50,7 +50,7 @@ namespace MediaBrowser.Controller.Playlists /// The user identifier. /// The share. /// Task. - Task RemoveFromShares(Guid playlistId, Guid userId, UserPermissions share); + Task RemoveFromShares(Guid playlistId, Guid userId, PlaylistUserPermissions share); /// /// Creates the playlist. diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index dfd9b83302..b948d2e18b 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -40,7 +40,7 @@ namespace MediaBrowser.Controller.Playlists public bool OpenAccess { get; set; } - public IReadOnlyList Shares { get; set; } + public IReadOnlyList Shares { get; set; } [JsonIgnore] public bool IsFile => IsPlaylistFile(Path); diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index 4ee1b2ef69..22ae3f12b2 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -519,7 +519,7 @@ namespace MediaBrowser.LocalMetadata.Parsers private void FetchFromSharesNode(XmlReader reader, IHasShares item) { - var list = new List(); + var list = new List(); reader.MoveToContent(); reader.Read(); @@ -830,7 +830,7 @@ namespace MediaBrowser.LocalMetadata.Parsers /// /// The xml reader. /// The share. - protected UserPermissions? GetShare(XmlReader reader) + protected PlaylistUserPermissions? GetShare(XmlReader reader) { reader.MoveToContent(); reader.Read(); @@ -864,7 +864,7 @@ namespace MediaBrowser.LocalMetadata.Parsers // This is valid if (!string.IsNullOrWhiteSpace(userId)) { - return new UserPermissions(userId, canEdit); + return new PlaylistUserPermissions(userId, canEdit); } return null; diff --git a/MediaBrowser.Model/Entities/IHasShares.cs b/MediaBrowser.Model/Entities/IHasShares.cs index fb6b6e424b..8c4ba6c425 100644 --- a/MediaBrowser.Model/Entities/IHasShares.cs +++ b/MediaBrowser.Model/Entities/IHasShares.cs @@ -10,5 +10,5 @@ public interface IHasShares /// /// Gets or sets the shares. /// - IReadOnlyList Shares { get; set; } + IReadOnlyList Shares { get; set; } } diff --git a/MediaBrowser.Model/Entities/UserPermissions.cs b/MediaBrowser.Model/Entities/PlaylistUserPermissions.cs similarity index 62% rename from MediaBrowser.Model/Entities/UserPermissions.cs rename to MediaBrowser.Model/Entities/PlaylistUserPermissions.cs index 80e2cd32c2..b5f017d2bf 100644 --- a/MediaBrowser.Model/Entities/UserPermissions.cs +++ b/MediaBrowser.Model/Entities/PlaylistUserPermissions.cs @@ -1,16 +1,18 @@ +using System; + namespace MediaBrowser.Model.Entities; /// -/// Class to hold data on user permissions for lists. +/// Class to hold data on user permissions for playlists. /// -public class UserPermissions +public class PlaylistUserPermissions { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The user id. /// Edit permission. - public UserPermissions(string userId, bool canEdit = false) + public PlaylistUserPermissions(Guid userId, bool canEdit = false) { UserId = userId; CanEdit = canEdit; @@ -19,7 +21,7 @@ public class UserPermissions /// /// Gets or sets the user id. /// - public string UserId { get; set; } + public Guid UserId { get; set; } /// /// Gets or sets a value indicating whether the user has edit permissions. diff --git a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs index f1351588fb..ec54b1afd3 100644 --- a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs +++ b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs @@ -33,7 +33,7 @@ public class PlaylistCreationRequest /// /// Gets or sets the user permissions. /// - public IReadOnlyList Users { get; set; } = []; + public IReadOnlyList Users { get; set; } = []; /// /// Gets or sets a value indicating whether the playlist is public. From c1dbb49315f90bf03445a960eb8eace86f1ea6f2 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Mon, 1 Apr 2024 20:43:05 +0200 Subject: [PATCH 021/444] Implement update endpoint --- .../Playlists/PlaylistManager.cs | 88 ++++++++++++------- .../Controllers/PlaylistsController.cs | 88 +++++++++++-------- .../Models/PlaylistDtos/UpdatePlaylistDto.cs | 34 +++++++ .../Migrations/Routines/FixPlaylistOwner.cs | 4 +- .../Playlists/IPlaylistManager.cs | 29 +++--- MediaBrowser.Controller/Playlists/Playlist.cs | 2 +- .../Parsers/BaseItemXmlParser.cs | 4 +- .../Savers/BaseXmlSaver.cs | 19 ++-- .../Playlists/PlaylistUpdateRequest.cs | 41 +++++++++ 9 files changed, 209 insertions(+), 100 deletions(-) create mode 100644 Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs create mode 100644 MediaBrowser.Model/Playlists/PlaylistUpdateRequest.cs diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 0e5add635d..517ec68dce 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -71,56 +71,56 @@ namespace Emby.Server.Implementations.Playlists return GetPlaylistsFolder(userId).GetChildren(user, true).OfType(); } - public async Task CreatePlaylist(PlaylistCreationRequest options) + public async Task CreatePlaylist(PlaylistCreationRequest request) { - var name = options.Name; + var name = request.Name; var folderName = _fileSystem.GetValidFilename(name); - var parentFolder = GetPlaylistsFolder(options.UserId); + var parentFolder = GetPlaylistsFolder(request.UserId); if (parentFolder is null) { throw new ArgumentException(nameof(parentFolder)); } - if (options.MediaType is null || options.MediaType == MediaType.Unknown) + if (request.MediaType is null || request.MediaType == MediaType.Unknown) { - foreach (var itemId in options.ItemIdList) + foreach (var itemId in request.ItemIdList) { var item = _libraryManager.GetItemById(itemId) ?? throw new ArgumentException("No item exists with the supplied Id"); if (item.MediaType != MediaType.Unknown) { - options.MediaType = item.MediaType; + request.MediaType = item.MediaType; } else if (item is MusicArtist || item is MusicAlbum || item is MusicGenre) { - options.MediaType = MediaType.Audio; + request.MediaType = MediaType.Audio; } else if (item is Genre) { - options.MediaType = MediaType.Video; + request.MediaType = MediaType.Video; } else { if (item is Folder folder) { - options.MediaType = folder.GetRecursiveChildren(i => !i.IsFolder && i.SupportsAddingToPlaylist) + request.MediaType = folder.GetRecursiveChildren(i => !i.IsFolder && i.SupportsAddingToPlaylist) .Select(i => i.MediaType) .FirstOrDefault(i => i != MediaType.Unknown); } } - if (options.MediaType is not null && options.MediaType != MediaType.Unknown) + if (request.MediaType is not null && request.MediaType != MediaType.Unknown) { break; } } } - if (options.MediaType is null || options.MediaType == MediaType.Unknown) + if (request.MediaType is null || request.MediaType == MediaType.Unknown) { - options.MediaType = MediaType.Audio; + request.MediaType = MediaType.Audio; } - var user = _userManager.GetUserById(options.UserId); + var user = _userManager.GetUserById(request.UserId); var path = Path.Combine(parentFolder.Path, folderName); path = GetTargetPath(path); @@ -133,20 +133,20 @@ namespace Emby.Server.Implementations.Playlists { Name = name, Path = path, - OwnerUserId = options.UserId, - Shares = options.Users ?? [], - OpenAccess = options.Public ?? false + OwnerUserId = request.UserId, + Shares = request.Users ?? [], + OpenAccess = request.Public ?? false }; - playlist.SetMediaType(options.MediaType); + playlist.SetMediaType(request.MediaType); parentFolder.AddChild(playlist); await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, CancellationToken.None) .ConfigureAwait(false); - if (options.ItemIdList.Count > 0) + if (request.ItemIdList.Count > 0) { - await AddToPlaylistInternal(playlist.Id, options.ItemIdList, user, new DtoOptions(false) + await AddToPlaylistInternal(playlist.Id, request.ItemIdList, user, new DtoOptions(false) { EnableImages = true }).ConfigureAwait(false); @@ -233,7 +233,7 @@ namespace Emby.Server.Implementations.Playlists // Update the playlist in the repository playlist.LinkedChildren = newLinkedChildren; - await UpdatePlaylist(playlist).ConfigureAwait(false); + await UpdatePlaylistInternal(playlist).ConfigureAwait(false); // Refresh playlist metadata _providerManager.QueueRefresh( @@ -262,7 +262,7 @@ namespace Emby.Server.Implementations.Playlists .Select(i => i.Item1) .ToArray(); - await UpdatePlaylist(playlist).ConfigureAwait(false); + await UpdatePlaylistInternal(playlist).ConfigureAwait(false); _providerManager.QueueRefresh( playlist.Id, @@ -306,7 +306,7 @@ namespace Emby.Server.Implementations.Playlists playlist.LinkedChildren = [.. newList]; - await UpdatePlaylist(playlist).ConfigureAwait(false); + await UpdatePlaylistInternal(playlist).ConfigureAwait(false); } /// @@ -530,7 +530,7 @@ namespace Emby.Server.Implementations.Playlists { playlist.OwnerUserId = rankedShares[0].UserId; playlist.Shares = rankedShares.Skip(1).ToArray(); - await UpdatePlaylist(playlist).ConfigureAwait(false); + await UpdatePlaylistInternal(playlist).ConfigureAwait(false); } else if (!playlist.OpenAccess) { @@ -548,12 +548,40 @@ namespace Emby.Server.Implementations.Playlists } } - public async Task ToggleOpenAccess(Guid playlistId, Guid userId) + public async Task UpdatePlaylist(PlaylistUpdateRequest request) { - var playlist = GetPlaylist(userId, playlistId); - playlist.OpenAccess = !playlist.OpenAccess; + var playlist = GetPlaylist(request.UserId, request.Id); - await UpdatePlaylist(playlist).ConfigureAwait(false); + if (request.Ids is not null) + { + playlist.LinkedChildren = []; + await UpdatePlaylistInternal(playlist).ConfigureAwait(false); + + var user = _userManager.GetUserById(request.UserId); + await AddToPlaylistInternal(request.Id, request.Ids, user, new DtoOptions(false) + { + EnableImages = true + }).ConfigureAwait(false); + + playlist = GetPlaylist(request.UserId, request.Id); + } + + if (request.Name is not null) + { + playlist.Name = request.Name; + } + + if (request.Users is not null) + { + playlist.Shares = request.Users; + } + + if (request.Public is not null) + { + playlist.OpenAccess = request.Public.Value; + } + + await UpdatePlaylistInternal(playlist).ConfigureAwait(false); } public async Task AddToShares(Guid playlistId, Guid userId, PlaylistUserPermissions share) @@ -568,7 +596,7 @@ namespace Emby.Server.Implementations.Playlists shares.Add(share); playlist.Shares = shares; - await UpdatePlaylist(playlist).ConfigureAwait(false); + await UpdatePlaylistInternal(playlist).ConfigureAwait(false); } public async Task RemoveFromShares(Guid playlistId, Guid userId, PlaylistUserPermissions share) @@ -577,10 +605,10 @@ namespace Emby.Server.Implementations.Playlists var shares = playlist.Shares.ToList(); shares.Remove(share); playlist.Shares = shares; - await UpdatePlaylist(playlist).ConfigureAwait(false); + await UpdatePlaylistInternal(playlist).ConfigureAwait(false); } - private async Task UpdatePlaylist(Playlist playlist) + private async Task UpdatePlaylistInternal(Playlist playlist) { await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 862e5235ee..de4c542d94 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -100,6 +100,54 @@ public class PlaylistsController : BaseJellyfinApiController return result; } + /// + /// Updates a playlist. + /// + /// The playlist id. + /// The id. + /// Playlist updated. + /// Unauthorized access. + /// Playlist not found. + /// + /// A that represents the asynchronous operation to update a playlist. + /// The task result contains an indicating success. + /// + [HttpPost("{playlistId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task UpdatePlaylist( + [FromRoute, Required] Guid playlistId, + [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Disallow)] UpdatePlaylistDto updatePlaylistRequest) + { + var callingUserId = User.GetUserId(); + + var playlist = _playlistManager.GetPlaylist(callingUserId, playlistId); + if (playlist is null) + { + return NotFound("Playlist not found"); + } + + var isPermitted = playlist.OwnerUserId.Equals(callingUserId) + || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)); + + if (!isPermitted) + { + return Unauthorized("Unauthorized access"); + } + + await _playlistManager.UpdatePlaylist(new PlaylistUpdateRequest + { + UserId = callingUserId, + Id = playlistId, + Name = updatePlaylistRequest.Name, + Ids = updatePlaylistRequest.Ids, + Users = updatePlaylistRequest.Users, + Public = updatePlaylistRequest.Public + }).ConfigureAwait(false); + + return NoContent(); + } + /// /// Get a playlist's users. /// @@ -131,44 +179,6 @@ public class PlaylistsController : BaseJellyfinApiController return isPermitted ? playlist.Shares.ToList() : Unauthorized("Unauthorized Access"); } - /// - /// Toggles public access of a playlist. - /// - /// The playlist id. - /// Public access toggled. - /// Unauthorized access. - /// Playlist not found. - /// - /// A that represents the asynchronous operation to toggle public access of a playlist. - /// The task result contains an indicating success. - /// - [HttpPost("{playlistId}/TogglePublic")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task TogglePublicAccess( - [FromRoute, Required] Guid playlistId) - { - var callingUserId = User.GetUserId(); - - var playlist = _playlistManager.GetPlaylist(callingUserId, playlistId); - if (playlist is null) - { - return NotFound("Playlist not found"); - } - - var isPermitted = playlist.OwnerUserId.Equals(callingUserId) - || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)); - - if (!isPermitted) - { - return Unauthorized("Unauthorized access"); - } - - await _playlistManager.ToggleOpenAccess(playlistId, callingUserId).ConfigureAwait(false); - - return NoContent(); - } - /// /// Modify a user to a playlist's users. /// @@ -206,7 +216,7 @@ public class PlaylistsController : BaseJellyfinApiController return Unauthorized("Unauthorized access"); } - await _playlistManager.AddToShares(playlistId, callingUserId, new PlaylistUserPermissions(userId.ToString(), canEdit)).ConfigureAwait(false); + await _playlistManager.AddToShares(playlistId, callingUserId, new PlaylistUserPermissions(userId, canEdit)).ConfigureAwait(false); return NoContent(); } diff --git a/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs new file mode 100644 index 0000000000..93e544eed8 --- /dev/null +++ b/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Jellyfin.Extensions.Json.Converters; +using MediaBrowser.Model.Entities; + +namespace Jellyfin.Api.Models.PlaylistDtos; + +/// +/// Updateexisting playlist dto. +/// +public class UpdatePlaylistDto +{ + /// + /// Gets or sets the name of the new playlist. + /// + public string? Name { get; set; } + + /// + /// Gets or sets item ids of the playlist. + /// + [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + public IReadOnlyList? Ids { get; set; } + + /// + /// Gets or sets the playlist users. + /// + public IReadOnlyList? Users { get; set; } + + /// + /// Gets or sets a value indicating whether the playlist is public. + /// + public bool? Public { get; set; } +} diff --git a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs index 06596c171f..3655a610d3 100644 --- a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs +++ b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs @@ -57,9 +57,9 @@ internal class FixPlaylistOwner : IMigrationRoutine if (shares.Count > 0) { var firstEditShare = shares.First(x => x.CanEdit); - if (firstEditShare is not null && Guid.TryParse(firstEditShare.UserId, out var guid)) + if (firstEditShare is not null) { - playlist.OwnerUserId = guid; + playlist.OwnerUserId = firstEditShare.UserId; playlist.Shares = shares.Where(x => x != firstEditShare).ToArray(); playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult(); _playlistManager.SavePlaylistFile(playlist); diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index 1750be6199..464620427a 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -19,6 +19,20 @@ namespace MediaBrowser.Controller.Playlists /// Playlist. Playlist GetPlaylist(Guid userId, Guid playlistId); + /// + /// Creates the playlist. + /// + /// The . + /// Task<Playlist>. + Task CreatePlaylist(PlaylistCreationRequest request); + + /// + /// Updates a playlist. + /// + /// The . + /// Task. + Task UpdatePlaylist(PlaylistUpdateRequest request); + /// /// Gets the playlists. /// @@ -26,14 +40,6 @@ namespace MediaBrowser.Controller.Playlists /// IEnumerable<Playlist>. IEnumerable GetPlaylists(Guid userId); - /// - /// Toggle OpenAccess policy of the playlist. - /// - /// The playlist identifier. - /// The user identifier. - /// Task. - Task ToggleOpenAccess(Guid playlistId, Guid userId); - /// /// Adds a share to the playlist. /// @@ -52,13 +58,6 @@ namespace MediaBrowser.Controller.Playlists /// Task. Task RemoveFromShares(Guid playlistId, Guid userId, PlaylistUserPermissions share); - /// - /// Creates the playlist. - /// - /// The options. - /// Task<Playlist>. - Task CreatePlaylist(PlaylistCreationRequest options); - /// /// Adds to playlist. /// diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index b948d2e18b..747dd9f637 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -252,7 +252,7 @@ namespace MediaBrowser.Controller.Playlists return false; } - return shares.Any(share => Guid.TryParse(share.UserId, out var id) && id.Equals(userId)); + return shares.Any(s => s.UserId.Equals(userId)); } public override bool IsVisibleStandalone(User user) diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index 22ae3f12b2..a7e027d94a 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -862,9 +862,9 @@ namespace MediaBrowser.LocalMetadata.Parsers } // This is valid - if (!string.IsNullOrWhiteSpace(userId)) + if (!string.IsNullOrWhiteSpace(userId) && Guid.TryParse(userId, out var guid)) { - return new PlaylistUserPermissions(userId, canEdit); + return new PlaylistUserPermissions(guid, canEdit); } return null; diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs index 5a71930799..ee0d10bea9 100644 --- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs @@ -420,19 +420,16 @@ namespace MediaBrowser.LocalMetadata.Savers foreach (var share in item.Shares) { - if (share.UserId is not null) - { - await writer.WriteStartElementAsync(null, "Share", null).ConfigureAwait(false); + await writer.WriteStartElementAsync(null, "Share", null).ConfigureAwait(false); - await writer.WriteElementStringAsync(null, "UserId", null, share.UserId).ConfigureAwait(false); - await writer.WriteElementStringAsync( - null, - "CanEdit", - null, - share.CanEdit.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()).ConfigureAwait(false); + await writer.WriteElementStringAsync(null, "UserId", null, share.UserId.ToString()).ConfigureAwait(false); + await writer.WriteElementStringAsync( + null, + "CanEdit", + null, + share.CanEdit.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()).ConfigureAwait(false); - await writer.WriteEndElementAsync().ConfigureAwait(false); - } + await writer.WriteEndElementAsync().ConfigureAwait(false); } await writer.WriteEndElementAsync().ConfigureAwait(false); diff --git a/MediaBrowser.Model/Playlists/PlaylistUpdateRequest.cs b/MediaBrowser.Model/Playlists/PlaylistUpdateRequest.cs new file mode 100644 index 0000000000..f574e679c3 --- /dev/null +++ b/MediaBrowser.Model/Playlists/PlaylistUpdateRequest.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Model.Playlists; + +/// +/// A playlist creation request. +/// +public class PlaylistUpdateRequest +{ + /// + /// Gets or sets the id of the playlist. + /// + public Guid Id { get; set; } + + /// + /// Gets or sets the id of the user updating the playlist. + /// + public Guid UserId { get; set; } + + /// + /// Gets or sets the name of the playlist. + /// + public string? Name { get; set; } + + /// + /// Gets or sets item ids to add to the playlist. + /// + public IReadOnlyList? Ids { get; set; } + + /// + /// Gets or sets the playlist users. + /// + public IReadOnlyList? Users { get; set; } + + /// + /// Gets or sets a value indicating whether the playlist is public. + /// + public bool? Public { get; set; } +} From ec36aaa73a776b28e8c311a7b049bf12d41761c8 Mon Sep 17 00:00:00 2001 From: Bas <44002186+854562@users.noreply.github.com> Date: Mon, 1 Apr 2024 17:59:04 +0000 Subject: [PATCH 022/444] Translated using Weblate (Dutch) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/ --- Emby.Server.Implementations/Localization/Core/nl.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index a925b71345..894d4b8ea9 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -126,5 +126,7 @@ "External": "Extern", "HearingImpaired": "Slechthorend", "TaskRefreshTrickplayImages": "Trickplay-afbeeldingen genereren", - "TaskRefreshTrickplayImagesDescription": "Creëert trickplay-voorvertoningen voor video's in bibliotheken waarvoor dit is ingeschakeld." + "TaskRefreshTrickplayImagesDescription": "Creëert trickplay-voorvertoningen voor video's in bibliotheken waarvoor dit is ingeschakeld.", + "TaskCleanCollectionsAndPlaylists": "Collecties en afspeellijsten opruimen", + "TaskCleanCollectionsAndPlaylistsDescription": "Verwijdert niet langer bestaande items uit collecties en afspeellijsten." } From 8cf77424f6c432386fb06017eccfa0e9f880a3ba Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 2 Apr 2024 08:08:36 +0200 Subject: [PATCH 023/444] Apply review suggestions --- .../Playlists/PlaylistManager.cs | 24 +++++++------- .../Controllers/PlaylistsController.cs | 31 +++++++++++-------- .../Models/PlaylistDtos/UpdatePlaylistDto.cs | 2 +- .../PlaylistDtos/UpdatePlaylistUserDto.cs | 12 +++++++ .../Playlists/IPlaylistManager.cs | 18 +++++------ .../Playlists/PlaylistUpdateRequest.cs | 2 +- .../Playlists/PlaylistUserUpdateRequest.cs | 24 ++++++++++++++ 7 files changed, 77 insertions(+), 36 deletions(-) create mode 100644 Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistUserDto.cs create mode 100644 MediaBrowser.Model/Playlists/PlaylistUserUpdateRequest.cs diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 517ec68dce..7a6cf9efff 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -22,6 +22,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Playlists; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using PlaylistsNET.Content; @@ -59,7 +60,7 @@ namespace Emby.Server.Implementations.Playlists _appConfig = appConfig; } - public Playlist GetPlaylist(Guid userId, Guid playlistId) + public Playlist GetPlaylistForUser(Guid playlistId, Guid userId) { return GetPlaylists(userId).Where(p => p.Id.Equals(playlistId)).FirstOrDefault(); } @@ -178,7 +179,7 @@ namespace Emby.Server.Implementations.Playlists return Playlist.GetPlaylistItems(playlistMediaType, items, user, options); } - public Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection itemIds, Guid userId) + public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection itemIds, Guid userId) { var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId); @@ -245,7 +246,7 @@ namespace Emby.Server.Implementations.Playlists RefreshPriority.High); } - public async Task RemoveFromPlaylistAsync(string playlistId, IEnumerable entryIds) + public async Task RemoveItemFromPlaylistAsync(string playlistId, IEnumerable entryIds) { if (_libraryManager.GetItemById(playlistId) is not Playlist playlist) { @@ -550,7 +551,7 @@ namespace Emby.Server.Implementations.Playlists public async Task UpdatePlaylist(PlaylistUpdateRequest request) { - var playlist = GetPlaylist(request.UserId, request.Id); + var playlist = GetPlaylistForUser(request.Id, request.UserId); if (request.Ids is not null) { @@ -563,7 +564,7 @@ namespace Emby.Server.Implementations.Playlists EnableImages = true }).ConfigureAwait(false); - playlist = GetPlaylist(request.UserId, request.Id); + playlist = GetPlaylistForUser(request.Id, request.UserId); } if (request.Name is not null) @@ -584,24 +585,25 @@ namespace Emby.Server.Implementations.Playlists await UpdatePlaylistInternal(playlist).ConfigureAwait(false); } - public async Task AddToShares(Guid playlistId, Guid userId, PlaylistUserPermissions share) + public async Task AddUserToShares(PlaylistUserUpdateRequest request) { - var playlist = GetPlaylist(userId, playlistId); + var userId = request.UserId; + var playlist = GetPlaylistForUser(request.Id, userId); var shares = playlist.Shares.ToList(); - var existingUserShare = shares.FirstOrDefault(s => s.UserId.Equals(share.UserId)); + var existingUserShare = shares.FirstOrDefault(s => s.UserId.Equals(userId)); if (existingUserShare is not null) { shares.Remove(existingUserShare); } - shares.Add(share); + shares.Add(new PlaylistUserPermissions(userId, request.CanEdit ?? false)); playlist.Shares = shares; await UpdatePlaylistInternal(playlist).ConfigureAwait(false); } - public async Task RemoveFromShares(Guid playlistId, Guid userId, PlaylistUserPermissions share) + public async Task RemoveUserFromShares(Guid playlistId, Guid userId, PlaylistUserPermissions share) { - var playlist = GetPlaylist(userId, playlistId); + var playlist = GetPlaylistForUser(playlistId, userId); var shares = playlist.Shares.ToList(); shares.Remove(share); playlist.Shares = shares; diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index de4c542d94..12186e02e6 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -121,7 +121,7 @@ public class PlaylistsController : BaseJellyfinApiController { var callingUserId = User.GetUserId(); - var playlist = _playlistManager.GetPlaylist(callingUserId, playlistId); + var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId); if (playlist is null) { return NotFound("Playlist not found"); @@ -167,7 +167,7 @@ public class PlaylistsController : BaseJellyfinApiController { var userId = User.GetUserId(); - var playlist = _playlistManager.GetPlaylist(userId, playlistId); + var playlist = _playlistManager.GetPlaylistForUser(playlistId, userId); if (playlist is null) { return NotFound("Playlist not found"); @@ -184,7 +184,7 @@ public class PlaylistsController : BaseJellyfinApiController /// /// The playlist id. /// The user id. - /// Edit permission. + /// The . /// User's permissions modified. /// Unauthorized access. /// Playlist not found. @@ -195,14 +195,14 @@ public class PlaylistsController : BaseJellyfinApiController [HttpPost("{playlistId}/User/{userId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task ModifyPlaylistUserPermissions( + public async Task UpdatePlaylistUser( [FromRoute, Required] Guid playlistId, [FromRoute, Required] Guid userId, - [FromBody] bool canEdit) + [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] UpdatePlaylistUserDto updatePlaylistUserRequest) { var callingUserId = User.GetUserId(); - var playlist = _playlistManager.GetPlaylist(callingUserId, playlistId); + var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId); if (playlist is null) { return NotFound("Playlist not found"); @@ -216,7 +216,12 @@ public class PlaylistsController : BaseJellyfinApiController return Unauthorized("Unauthorized access"); } - await _playlistManager.AddToShares(playlistId, callingUserId, new PlaylistUserPermissions(userId, canEdit)).ConfigureAwait(false); + await _playlistManager.AddUserToShares(new PlaylistUserUpdateRequest + { + Id = playlistId, + UserId = userId, + CanEdit = updatePlaylistUserRequest.CanEdit + }).ConfigureAwait(false); return NoContent(); } @@ -243,7 +248,7 @@ public class PlaylistsController : BaseJellyfinApiController { var callingUserId = User.GetUserId(); - var playlist = _playlistManager.GetPlaylist(callingUserId, playlistId); + var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId); if (playlist is null) { return NotFound("Playlist not found"); @@ -263,7 +268,7 @@ public class PlaylistsController : BaseJellyfinApiController return NotFound("User permissions not found"); } - await _playlistManager.RemoveFromShares(playlistId, callingUserId, share).ConfigureAwait(false); + await _playlistManager.RemoveUserFromShares(playlistId, callingUserId, share).ConfigureAwait(false); return NoContent(); } @@ -278,13 +283,13 @@ public class PlaylistsController : BaseJellyfinApiController /// An on success. [HttpPost("{playlistId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task AddToPlaylist( + public async Task AddItemToPlaylist( [FromRoute, Required] Guid playlistId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, [FromQuery] Guid? userId) { userId = RequestHelpers.GetUserId(User, userId); - await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId.Value).ConfigureAwait(false); + await _playlistManager.AddItemToPlaylistAsync(playlistId, ids, userId.Value).ConfigureAwait(false); return NoContent(); } @@ -316,11 +321,11 @@ public class PlaylistsController : BaseJellyfinApiController /// An on success. [HttpDelete("{playlistId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task RemoveFromPlaylist( + public async Task RemoveItemFromPlaylist( [FromRoute, Required] string playlistId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds) { - await _playlistManager.RemoveFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false); + await _playlistManager.RemoveItemFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false); return NoContent(); } diff --git a/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs index 93e544eed8..0e109db3ee 100644 --- a/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs +++ b/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs @@ -7,7 +7,7 @@ using MediaBrowser.Model.Entities; namespace Jellyfin.Api.Models.PlaylistDtos; /// -/// Updateexisting playlist dto. +/// Update existing playlist dto. Fields set to `null` will not be updated and keep their current values. /// public class UpdatePlaylistDto { diff --git a/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistUserDto.cs b/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistUserDto.cs new file mode 100644 index 0000000000..60467b5e70 --- /dev/null +++ b/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistUserDto.cs @@ -0,0 +1,12 @@ +namespace Jellyfin.Api.Models.PlaylistDtos; + +/// +/// Update existing playlist user dto. Fields set to `null` will not be updated and keep their current values. +/// +public class UpdatePlaylistUserDto +{ + /// + /// Gets or sets a value indicating whether the user can edit the playlist. + /// + public bool? CanEdit { get; set; } +} diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index 464620427a..821b901a03 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -14,10 +14,10 @@ namespace MediaBrowser.Controller.Playlists /// /// Gets the playlist. /// - /// The user identifier. /// The playlist identifier. + /// The user identifier. /// Playlist. - Playlist GetPlaylist(Guid userId, Guid playlistId); + Playlist GetPlaylistForUser(Guid playlistId, Guid userId); /// /// Creates the playlist. @@ -43,20 +43,18 @@ namespace MediaBrowser.Controller.Playlists /// /// Adds a share to the playlist. /// - /// The playlist identifier. - /// The user identifier. - /// The share. + /// The . /// Task. - Task AddToShares(Guid playlistId, Guid userId, PlaylistUserPermissions share); + Task AddUserToShares(PlaylistUserUpdateRequest request); /// - /// Rremoves a share from the playlist. + /// Removes a share from the playlist. /// /// The playlist identifier. /// The user identifier. /// The share. /// Task. - Task RemoveFromShares(Guid playlistId, Guid userId, PlaylistUserPermissions share); + Task RemoveUserFromShares(Guid playlistId, Guid userId, PlaylistUserPermissions share); /// /// Adds to playlist. @@ -65,7 +63,7 @@ namespace MediaBrowser.Controller.Playlists /// The item ids. /// The user identifier. /// Task. - Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection itemIds, Guid userId); + Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection itemIds, Guid userId); /// /// Removes from playlist. @@ -73,7 +71,7 @@ namespace MediaBrowser.Controller.Playlists /// The playlist identifier. /// The entry ids. /// Task. - Task RemoveFromPlaylistAsync(string playlistId, IEnumerable entryIds); + Task RemoveItemFromPlaylistAsync(string playlistId, IEnumerable entryIds); /// /// Gets the playlists folder. diff --git a/MediaBrowser.Model/Playlists/PlaylistUpdateRequest.cs b/MediaBrowser.Model/Playlists/PlaylistUpdateRequest.cs index f574e679c3..db290bbdbf 100644 --- a/MediaBrowser.Model/Playlists/PlaylistUpdateRequest.cs +++ b/MediaBrowser.Model/Playlists/PlaylistUpdateRequest.cs @@ -5,7 +5,7 @@ using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.Playlists; /// -/// A playlist creation request. +/// A playlist update request. /// public class PlaylistUpdateRequest { diff --git a/MediaBrowser.Model/Playlists/PlaylistUserUpdateRequest.cs b/MediaBrowser.Model/Playlists/PlaylistUserUpdateRequest.cs new file mode 100644 index 0000000000..1840efdf37 --- /dev/null +++ b/MediaBrowser.Model/Playlists/PlaylistUserUpdateRequest.cs @@ -0,0 +1,24 @@ +using System; + +namespace MediaBrowser.Model.Playlists; + +/// +/// A playlist user update request. +/// +public class PlaylistUserUpdateRequest +{ + /// + /// Gets or sets the id of the playlist. + /// + public Guid Id { get; set; } + + /// + /// Gets or sets the id of the updated user. + /// + public Guid UserId { get; set; } + + /// + /// Gets or sets a value indicating whether the user can edit the playlist. + /// + public bool? CanEdit { get; set; } +} From 85cf91c4cf9eeef38dcc5f4705d74dd59116d293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kucharczyk?= Date: Tue, 2 Apr 2024 05:36:58 +0000 Subject: [PATCH 024/444] Translated using Weblate (Czech) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/cs/ --- Emby.Server.Implementations/Localization/Core/cs.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index 1c7bc75b5c..2fa1c19e3a 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -126,5 +126,7 @@ "External": "Externí", "HearingImpaired": "Sluchově postižení", "TaskRefreshTrickplayImages": "Generovat obrázky pro Trickplay", - "TaskRefreshTrickplayImagesDescription": "Obrázky Trickplay se používají k zobrazení náhledů u videí v knihovnách, kde je to povoleno." + "TaskRefreshTrickplayImagesDescription": "Obrázky Trickplay se používají k zobrazení náhledů u videí v knihovnách, kde je to povoleno.", + "TaskCleanCollectionsAndPlaylists": "Pročistit kolekce a seznamy přehrávání", + "TaskCleanCollectionsAndPlaylistsDescription": "Odstraní neexistující položky z kolekcí a seznamů přehrávání." } From 00499fa27bb61e2ac0e2e932f8406d27b020a08a Mon Sep 17 00:00:00 2001 From: Troja Date: Tue, 2 Apr 2024 05:50:57 +0000 Subject: [PATCH 025/444] Translated using Weblate (Belarusian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/be/ --- Emby.Server.Implementations/Localization/Core/be.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json index 05af8d8a5a..77643505e1 100644 --- a/Emby.Server.Implementations/Localization/Core/be.json +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -125,5 +125,7 @@ "TaskDownloadMissingSubtitles": "Спампаваць адсутныя субтытры", "TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных спісаў прайгравання HLS. Гэта задача можа працаваць у працягу доўгага часу.", "TaskRefreshTrickplayImages": "Стварыце выявы Trickplay", - "TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках." + "TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках.", + "TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і спісы прайгравання", + "TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і спісаў прайгравання, якія больш не існуюць." } From 0af101cbf71abcf51de53645ea3e3ce257debdcd Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 2 Apr 2024 21:03:58 +0800 Subject: [PATCH 026/444] fix: av1 codecs string (#11280) Co-authored-by: Dmitry Lyzo <56478732+dmitrylyzo@users.noreply.github.com> --- Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs index 5eec1b0ca6..ec67b4c1bf 100644 --- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs @@ -192,7 +192,7 @@ public static class HlsCodecStringHelpers /// The AV1 codec string. public static string GetAv1String(string? profile, int level, bool tierFlag, int bitDepth) { - // https://aomedia.org/av1/specification/annex-a/ + // https://aomediacodec.github.io/av1-isobmff/#codecsparam // FORMAT: [codecTag].[profile].[level][tier].[bitDepth] StringBuilder result = new StringBuilder("av01", 13); @@ -214,8 +214,7 @@ public static class HlsCodecStringHelpers result.Append(".0"); } - if (level <= 0 - || level > 31) + if (level is <= 0 or > 31) { // Default to the maximum defined level 6.3 level = 19; @@ -230,7 +229,8 @@ public static class HlsCodecStringHelpers } result.Append('.') - .Append(level) + // Needed to pad it double digits; otherwise, browsers will reject the stream. + .AppendFormat(CultureInfo.InvariantCulture, "{0:D2}", level) .Append(tierFlag ? 'H' : 'M'); string bitDepthD2 = bitDepth.ToString("D2", CultureInfo.InvariantCulture); From 444060037932cc2f2c66a28004e97631b887fff9 Mon Sep 17 00:00:00 2001 From: Caidy Date: Tue, 2 Apr 2024 21:04:25 +0800 Subject: [PATCH 027/444] fix: rtsp live stream ffprobe timeout (#11279) --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 6c43315a87..8076780250 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -463,6 +463,11 @@ namespace MediaBrowser.MediaEncoding.Encoder extraArgs += " -user_agent " + userAgent; } + if (request.MediaSource.Protocol == MediaProtocol.Rtsp) + { + extraArgs += " -rtsp_transport tcp+udp -rtsp_flags prefer_tcp"; + } + return extraArgs; } From b5acee65056f71c3ef8c1c5e46557c73cedcbebf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 07:04:40 -0600 Subject: [PATCH 028/444] chore(deps): update ci dependencies (#11226) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci-openapi.yml | 2 +- .github/workflows/commands.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 0864299f79..bdbfcd3eb3 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -105,7 +105,7 @@ jobs: body="${body//$'\r'/'%0D'}" echo ::set-output name=body::$body - name: Find difference comment - uses: peter-evans/find-comment@d5fe37641ad8451bdd80312415672ba26c86575e # v3.0.0 + uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0 id: find-comment with: issue-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index d78f11166c..5055bbfa53 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -132,7 +132,7 @@ jobs: with: repository: jellyfin/jellyfin-triage-script - name: install python - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 with: python-version: '3.12' cache: 'pip' From e53650fbb5256e0f234c1af1b7dfdbd06e619d85 Mon Sep 17 00:00:00 2001 From: myrad2267 Date: Tue, 2 Apr 2024 10:32:36 +0000 Subject: [PATCH 029/444] Translated using Weblate (French) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fr/ --- Emby.Server.Implementations/Localization/Core/fr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index d04a79de18..db83d4b47d 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -126,5 +126,7 @@ "External": "Externe", "HearingImpaired": "Malentendants", "TaskRefreshTrickplayImages": "Générer des images Trickplay", - "TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées." + "TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.", + "TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture", + "TaskCleanCollectionsAndPlaylistsDescription": "Supprime les éléments des collections et des listes de lecture qui n'existent plus." } From ab4dd6e58296a5952bb4ee8681860b6039bc8567 Mon Sep 17 00:00:00 2001 From: stanol Date: Tue, 2 Apr 2024 11:32:21 +0000 Subject: [PATCH 030/444] 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 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json index 3f7fca427b..5f97d1ef95 100644 --- a/Emby.Server.Implementations/Localization/Core/uk.json +++ b/Emby.Server.Implementations/Localization/Core/uk.json @@ -125,5 +125,7 @@ "External": "Зовнішній", "HearingImpaired": "З порушеннями слуху", "TaskRefreshTrickplayImagesDescription": "Створює trickplay-зображення для відео у ввімкнених медіатеках.", - "TaskRefreshTrickplayImages": "Створити Trickplay-зображення" + "TaskRefreshTrickplayImages": "Створити Trickplay-зображення", + "TaskCleanCollectionsAndPlaylists": "Очистити колекції і списки відтворення", + "TaskCleanCollectionsAndPlaylistsDescription": "Видаляє елементи з колекцій і списків відтворення, які більше не існують." } From 5179c7d158da52f8857599b398bc58e3760d846a Mon Sep 17 00:00:00 2001 From: nextlooper42 Date: Tue, 2 Apr 2024 15:08:53 +0000 Subject: [PATCH 031/444] Translated using Weblate (Slovak) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sk/ --- Emby.Server.Implementations/Localization/Core/sk.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json index 43594a42eb..905dba5ab0 100644 --- a/Emby.Server.Implementations/Localization/Core/sk.json +++ b/Emby.Server.Implementations/Localization/Core/sk.json @@ -126,5 +126,7 @@ "External": "Externé", "HearingImpaired": "Sluchovo postihnutí", "TaskRefreshTrickplayImages": "Generovanie obrázkov Trickplay", - "TaskRefreshTrickplayImagesDescription": "Vytvára trickplay náhľady pre videá v povolených knižniciach." + "TaskRefreshTrickplayImagesDescription": "Vytvára trickplay náhľady pre videá v povolených knižniciach.", + "TaskCleanCollectionsAndPlaylists": "Vyčistiť kolekcie a playlisty", + "TaskCleanCollectionsAndPlaylistsDescription": "Odstráni položky z kolekcií a playlistov, ktoré už neexistujú." } From 286232e21cd5f7644d940a180e9167d14546b912 Mon Sep 17 00:00:00 2001 From: Kityn Date: Tue, 2 Apr 2024 18:09:04 +0000 Subject: [PATCH 032/444] Translated using Weblate (Polish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pl/ --- Emby.Server.Implementations/Localization/Core/pl.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json index bd572b744b..64427b459d 100644 --- a/Emby.Server.Implementations/Localization/Core/pl.json +++ b/Emby.Server.Implementations/Localization/Core/pl.json @@ -126,5 +126,7 @@ "TaskKeyframeExtractor": "Ekstraktor klatek kluczowych", "HearingImpaired": "Niedosłyszący", "TaskRefreshTrickplayImages": "Generuj obrazy trickplay", - "TaskRefreshTrickplayImagesDescription": "Tworzy podglądy trickplay dla filmów we włączonych bibliotekach." + "TaskRefreshTrickplayImagesDescription": "Tworzy podglądy trickplay dla filmów we włączonych bibliotekach.", + "TaskCleanCollectionsAndPlaylistsDescription": "Usuwa elementy z kolekcji i list odtwarzania, które już nie istnieją.", + "TaskCleanCollectionsAndPlaylists": "Oczyść kolekcje i listy odtwarzania" } From 4a9565ab52c19be8798e622a3d238c3238b01c07 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 3 Apr 2024 14:56:56 +0200 Subject: [PATCH 033/444] Fix some spelling mistakes --- .../Library/MediaSourceManager.cs | 23 ++++++++++--------- .../Library/MediaStreamSelector.cs | 6 ++--- .../Library/IMediaSourceManager.cs | 2 +- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 18ada6aeb5..9658bd5665 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -191,7 +191,7 @@ namespace Emby.Server.Implementations.Library if (user is not null) { - SetDefaultAudioAndSubtitleStreamIndexes(item, source, user); + SetDefaultAudioAndSubtitleStreamIndices(item, source, user); if (item.MediaType == MediaType.Audio) { @@ -296,7 +296,7 @@ namespace Emby.Server.Implementations.Library catch (Exception ex) { _logger.LogError(ex, "Error getting media sources"); - return Enumerable.Empty(); + return []; } } @@ -339,7 +339,7 @@ namespace Emby.Server.Implementations.Library { foreach (var source in sources) { - SetDefaultAudioAndSubtitleStreamIndexes(item, source, user); + SetDefaultAudioAndSubtitleStreamIndices(item, source, user); if (item.MediaType == MediaType.Audio) { @@ -360,7 +360,7 @@ namespace Emby.Server.Implementations.Library { if (string.IsNullOrEmpty(language)) { - return Array.Empty(); + return []; } var culture = _localizationManager.FindLanguageInfo(language); @@ -369,14 +369,15 @@ namespace Emby.Server.Implementations.Library return culture.ThreeLetterISOLanguageNames; } - return new string[] { language }; + return [language]; } private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection) { if (userData.SubtitleStreamIndex.HasValue && user.RememberSubtitleSelections - && user.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection) + && user.SubtitleMode != SubtitlePlaybackMode.None + && allowRememberingSelection) { var index = userData.SubtitleStreamIndex.Value; // Make sure the saved index is still valid @@ -390,7 +391,7 @@ namespace Emby.Server.Implementations.Library var preferredSubs = NormalizeLanguage(user.SubtitleLanguagePreference); var defaultAudioIndex = source.DefaultAudioStreamIndex; - var audioLangage = defaultAudioIndex is null + var audioLanguage = defaultAudioIndex is null ? null : source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select(i => i.Language).FirstOrDefault(); @@ -398,9 +399,9 @@ namespace Emby.Server.Implementations.Library source.MediaStreams, preferredSubs, user.SubtitleMode, - audioLangage); + audioLanguage); - MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLangage); + MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLanguage); } private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection) @@ -421,7 +422,7 @@ namespace Emby.Server.Implementations.Library source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack); } - public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user) + public void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user) { // Item would only be null if the app didn't supply ItemId as part of the live stream open request var mediaType = item?.MediaType ?? MediaType.Video; @@ -526,7 +527,7 @@ namespace Emby.Server.Implementations.Library var item = request.ItemId.IsEmpty() ? null : _libraryManager.GetItemById(request.ItemId); - SetDefaultAudioAndSubtitleStreamIndexes(item, clone, user); + SetDefaultAudioAndSubtitleStreamIndices(item, clone, user); } return new Tuple(new LiveStreamResponse(clone), liveStream as IDirectStreamProvider); diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs index 6aef87c525..ea223e3ece 100644 --- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs +++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs @@ -124,16 +124,16 @@ namespace Emby.Server.Implementations.Library } else if (mode == SubtitlePlaybackMode.Always) { - // always load the most suitable full subtitles + // Always load the most suitable full subtitles filteredStreams = sortedStreams.Where(s => !s.IsForced).ToList(); } else if (mode == SubtitlePlaybackMode.OnlyForced) { - // always load the most suitable full subtitles + // Always load the most suitable full subtitles filteredStreams = sortedStreams.Where(s => s.IsForced).ToList(); } - // load forced subs if we have found no suitable full subtitles + // Load forced subs if we have found no suitable full subtitles var iterStreams = filteredStreams is null || filteredStreams.Count == 0 ? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) : filteredStreams; diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs index bace703ada..44a1a85e30 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs @@ -138,7 +138,7 @@ namespace MediaBrowser.Controller.Library MediaProtocol GetPathProtocol(string path); - void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user); + void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user); Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProbeDelay, bool isLiveStream, CancellationToken cancellationToken); } From 2428672599763f47d9ac0fc74d9e777e40f7345b Mon Sep 17 00:00:00 2001 From: queeup Date: Wed, 3 Apr 2024 12:54:02 +0000 Subject: [PATCH 034/444] Translated using Weblate (Turkish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/tr/ --- Emby.Server.Implementations/Localization/Core/tr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index d7a627d127..0597539578 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -126,5 +126,7 @@ "External": "Harici", "HearingImpaired": "Duyma Engelli", "TaskRefreshTrickplayImages": "Trickplay Görselleri Oluştur", - "TaskRefreshTrickplayImagesDescription": "Etkin kütüphanelerdeki videolar için trickplay önizlemeleri oluşturur." + "TaskRefreshTrickplayImagesDescription": "Etkin kütüphanelerdeki videolar için trickplay önizlemeleri oluşturur.", + "TaskCleanCollectionsAndPlaylistsDescription": "Artık var olmayan koleksiyon ve çalma listelerindeki ögeleri kaldırır.", + "TaskCleanCollectionsAndPlaylists": "Koleksiyonları ve çalma listelerini temizleyin" } From 3e0b201688d6efeca5c65df11425001f73c8c9ec Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 3 Apr 2024 16:06:20 +0200 Subject: [PATCH 035/444] Enforce permissions --- .../Controllers/PlaylistsController.cs | 105 +++++++++++++++--- .../Playlists/IPlaylistManager.cs | 2 +- 2 files changed, 88 insertions(+), 19 deletions(-) diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 12186e02e6..4ced64ae70 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -106,7 +106,7 @@ public class PlaylistsController : BaseJellyfinApiController /// The playlist id. /// The id. /// Playlist updated. - /// Unauthorized access. + /// Access forbidden. /// Playlist not found. /// /// A that represents the asynchronous operation to update a playlist. @@ -114,10 +114,11 @@ public class PlaylistsController : BaseJellyfinApiController /// [HttpPost("{playlistId}")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task UpdatePlaylist( [FromRoute, Required] Guid playlistId, - [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Disallow)] UpdatePlaylistDto updatePlaylistRequest) + [FromBody, Required] UpdatePlaylistDto updatePlaylistRequest) { var callingUserId = User.GetUserId(); @@ -132,7 +133,7 @@ public class PlaylistsController : BaseJellyfinApiController if (!isPermitted) { - return Unauthorized("Unauthorized access"); + return Forbid(); } await _playlistManager.UpdatePlaylist(new PlaylistUpdateRequest @@ -153,14 +154,14 @@ public class PlaylistsController : BaseJellyfinApiController /// /// The playlist id. /// Found shares. - /// Unauthorized access. + /// Access forbidden. /// Playlist not found. /// /// A list of objects. /// [HttpGet("{playlistId}/User")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetPlaylistUsers( [FromRoute, Required] Guid playlistId) @@ -173,10 +174,9 @@ public class PlaylistsController : BaseJellyfinApiController return NotFound("Playlist not found"); } - var isPermitted = playlist.OwnerUserId.Equals(userId) - || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(userId)); + var isPermitted = playlist.OwnerUserId.Equals(userId); - return isPermitted ? playlist.Shares.ToList() : Unauthorized("Unauthorized Access"); + return isPermitted ? playlist.Shares.ToList() : Forbid(); } /// @@ -186,7 +186,7 @@ public class PlaylistsController : BaseJellyfinApiController /// The user id. /// The . /// User's permissions modified. - /// Unauthorized access. + /// Access forbidden. /// Playlist not found. /// /// A that represents the asynchronous operation to modify an user's playlist permissions. @@ -194,7 +194,8 @@ public class PlaylistsController : BaseJellyfinApiController /// [HttpPost("{playlistId}/User/{userId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task UpdatePlaylistUser( [FromRoute, Required] Guid playlistId, [FromRoute, Required] Guid userId, @@ -208,12 +209,11 @@ public class PlaylistsController : BaseJellyfinApiController return NotFound("Playlist not found"); } - var isPermitted = playlist.OwnerUserId.Equals(callingUserId) - || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)); + var isPermitted = playlist.OwnerUserId.Equals(callingUserId); if (!isPermitted) { - return Unauthorized("Unauthorized access"); + return Forbid(); } await _playlistManager.AddUserToShares(new PlaylistUserUpdateRequest @@ -240,7 +240,7 @@ public class PlaylistsController : BaseJellyfinApiController /// [HttpDelete("{playlistId}/User/{userId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task RemoveUserFromPlaylist( [FromRoute, Required] Guid playlistId, @@ -259,7 +259,7 @@ public class PlaylistsController : BaseJellyfinApiController if (!isPermitted) { - return Unauthorized("Unauthorized access"); + return Forbid(); } var share = playlist.Shares.FirstOrDefault(s => s.UserId.Equals(userId)); @@ -280,15 +280,33 @@ public class PlaylistsController : BaseJellyfinApiController /// Item id, comma delimited. /// The userId. /// Items added to playlist. + /// Access forbidden. + /// Playlist not found. /// An on success. [HttpPost("{playlistId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task AddItemToPlaylist( [FromRoute, Required] Guid playlistId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, [FromQuery] Guid? userId) { userId = RequestHelpers.GetUserId(User, userId); + var playlist = _playlistManager.GetPlaylistForUser(playlistId, userId.Value); + if (playlist is null) + { + return NotFound("Playlist not found"); + } + + var isPermitted = playlist.OwnerUserId.Equals(userId.Value) + || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(userId.Value)); + + if (!isPermitted) + { + return Forbid(); + } + await _playlistManager.AddItemToPlaylistAsync(playlistId, ids, userId.Value).ConfigureAwait(false); return NoContent(); } @@ -300,14 +318,34 @@ public class PlaylistsController : BaseJellyfinApiController /// The item id. /// The new index. /// Item moved to new index. + /// Access forbidden. + /// Playlist not found. /// An on success. [HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task MoveItem( [FromRoute, Required] string playlistId, [FromRoute, Required] string itemId, [FromRoute, Required] int newIndex) { + var callingUserId = User.GetUserId(); + + var playlist = _playlistManager.GetPlaylistForUser(Guid.Parse(playlistId), callingUserId); + if (playlist is null) + { + return NotFound("Playlist not found"); + } + + var isPermitted = playlist.OwnerUserId.Equals(callingUserId) + || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)); + + if (!isPermitted) + { + return Forbid(); + } + await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex).ConfigureAwait(false); return NoContent(); } @@ -318,13 +356,33 @@ public class PlaylistsController : BaseJellyfinApiController /// The playlist id. /// The item ids, comma delimited. /// Items removed. + /// Access forbidden. + /// Playlist not found. /// An on success. [HttpDelete("{playlistId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task RemoveItemFromPlaylist( [FromRoute, Required] string playlistId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds) { + var callingUserId = User.GetUserId(); + + var playlist = _playlistManager.GetPlaylistForUser(Guid.Parse(playlistId), callingUserId); + if (playlist is null) + { + return NotFound("Playlist not found"); + } + + var isPermitted = playlist.OwnerUserId.Equals(callingUserId) + || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)); + + if (!isPermitted) + { + return Forbid(); + } + await _playlistManager.RemoveItemFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false); return NoContent(); } @@ -342,10 +400,12 @@ public class PlaylistsController : BaseJellyfinApiController /// Optional. The max number of images to return, per image type. /// Optional. The image types to include in the output. /// Original playlist returned. + /// Access forbidden. /// Playlist not found. /// The original playlist items. [HttpGet("{playlistId}/Items")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetPlaylistItems( [FromRoute, Required] Guid playlistId, @@ -359,10 +419,19 @@ public class PlaylistsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { userId = RequestHelpers.GetUserId(User, userId); - var playlist = (Playlist)_libraryManager.GetItemById(playlistId); + var playlist = _playlistManager.GetPlaylistForUser(playlistId, userId.Value); if (playlist is null) { - return NotFound(); + return NotFound("Playlist not found"); + } + + var isPermitted = playlist.OpenAccess + || playlist.OwnerUserId.Equals(userId.Value) + || playlist.Shares.Any(s => s.UserId.Equals(userId.Value)); + + if (!isPermitted) + { + return Forbid(); } var user = userId.IsNullOrEmpty() diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index 821b901a03..cbe4bd87f5 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -23,7 +23,7 @@ namespace MediaBrowser.Controller.Playlists /// Creates the playlist. /// /// The . - /// Task<Playlist>. + /// The created playlist. Task CreatePlaylist(PlaylistCreationRequest request); /// From 04c5b9d6800d7790d08958b9d6700299ba0c8e74 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 3 Apr 2024 16:14:06 +0200 Subject: [PATCH 036/444] Add endpoint to get user permissions --- .../Controllers/PlaylistsController.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 4ced64ae70..567a274123 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -179,6 +179,40 @@ public class PlaylistsController : BaseJellyfinApiController return isPermitted ? playlist.Shares.ToList() : Forbid(); } + /// + /// Get a playlist users. + /// + /// The playlist id. + /// The user id. + /// Found shares. + /// Access forbidden. + /// Playlist not found. + /// + /// . + /// + [HttpGet("{playlistId}/User/{userId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GetPlaylistUser( + [FromRoute, Required] Guid playlistId, + [FromRoute, Required] Guid userId) + { + var callingUserId = User.GetUserId(); + + var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId); + if (playlist is null) + { + return NotFound("Playlist not found"); + } + + var isPermitted = playlist.OwnerUserId.Equals(userId) + || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)) + || userId.Equals(callingUserId); + + return isPermitted ? playlist.Shares.FirstOrDefault(s => s.UserId.Equals(userId)) : Forbid(); + } + /// /// Modify a user to a playlist's users. /// From d72f40fe4159d83440c3362d137901e4c418c4b9 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 3 Apr 2024 16:19:13 +0200 Subject: [PATCH 037/444] Return 204 on OpenAccess --- Jellyfin.Api/Controllers/PlaylistsController.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 567a274123..d6239503c8 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -184,7 +184,8 @@ public class PlaylistsController : BaseJellyfinApiController /// /// The playlist id. /// The user id. - /// Found shares. + /// User permission found. + /// No user permission found but open access. /// Access forbidden. /// Playlist not found. /// @@ -192,6 +193,7 @@ public class PlaylistsController : BaseJellyfinApiController /// [HttpGet("{playlistId}/User/{userId}")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetPlaylistUser( @@ -210,7 +212,7 @@ public class PlaylistsController : BaseJellyfinApiController || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)) || userId.Equals(callingUserId); - return isPermitted ? playlist.Shares.FirstOrDefault(s => s.UserId.Equals(userId)) : Forbid(); + return isPermitted ? playlist.Shares.FirstOrDefault(s => s.UserId.Equals(userId)) : playlist.OpenAccess ? NoContent() : Forbid(); } /// From 247ec19de4945f4d7ab64d4a8772b72759e3d2b7 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 3 Apr 2024 16:23:14 +0200 Subject: [PATCH 038/444] Fixup --- Jellyfin.Api/Controllers/PlaylistsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index d6239503c8..54b2261ab7 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -208,7 +208,7 @@ public class PlaylistsController : BaseJellyfinApiController return NotFound("Playlist not found"); } - var isPermitted = playlist.OwnerUserId.Equals(userId) + var isPermitted = playlist.OwnerUserId.Equals(callingUserId) || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)) || userId.Equals(callingUserId); From 5396b616bf2d7e387bc20539eeae03e841f60c01 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 3 Apr 2024 16:32:25 +0200 Subject: [PATCH 039/444] Fixup --- Jellyfin.Api/Controllers/PlaylistsController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 54b2261ab7..01c1c578db 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -208,11 +208,12 @@ public class PlaylistsController : BaseJellyfinApiController return NotFound("Playlist not found"); } + var userPermission = playlist.Shares.FirstOrDefault(s => s.UserId.Equals(userId)); var isPermitted = playlist.OwnerUserId.Equals(callingUserId) || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)) || userId.Equals(callingUserId); - return isPermitted ? playlist.Shares.FirstOrDefault(s => s.UserId.Equals(userId)) : playlist.OpenAccess ? NoContent() : Forbid(); + return isPermitted ? userPermission is not null ? userPermission : NotFound("User permissions not found") : playlist.OpenAccess ? NoContent() : Forbid(); } /// From 3c7562313b3d0a8bdaaa6623215f2c979150cf5f Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 3 Apr 2024 16:57:10 +0200 Subject: [PATCH 040/444] Apply review suggestions --- Jellyfin.Api/Controllers/PlaylistsController.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 01c1c578db..d167d996c1 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -159,7 +159,7 @@ public class PlaylistsController : BaseJellyfinApiController /// /// A list of objects. /// - [HttpGet("{playlistId}/User")] + [HttpGet("{playlistId}/Users")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -191,7 +191,7 @@ public class PlaylistsController : BaseJellyfinApiController /// /// . /// - [HttpGet("{playlistId}/User/{userId}")] + [HttpGet("{playlistId}/Users/{userId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -229,7 +229,7 @@ public class PlaylistsController : BaseJellyfinApiController /// A that represents the asynchronous operation to modify an user's playlist permissions. /// The task result contains an indicating success. /// - [HttpPost("{playlistId}/User/{userId}")] + [HttpPost("{playlistId}/Users/{userId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -275,7 +275,7 @@ public class PlaylistsController : BaseJellyfinApiController /// A that represents the asynchronous operation to delete a user from a playlist's shares. /// The task result contains an indicating success. /// - [HttpDelete("{playlistId}/User/{userId}")] + [HttpDelete("{playlistId}/Users/{userId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] From 51e2faa448624a334128042cdca1e592ec97240e Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 3 Apr 2024 20:06:57 +0200 Subject: [PATCH 041/444] Apply review suggestions --- Jellyfin.Api/Controllers/PlaylistsController.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index d167d996c1..ca90d2a6d8 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -113,7 +113,7 @@ public class PlaylistsController : BaseJellyfinApiController /// The task result contains an indicating success. /// [HttpPost("{playlistId}")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task UpdatePlaylist( @@ -185,16 +185,12 @@ public class PlaylistsController : BaseJellyfinApiController /// The playlist id. /// The user id. /// User permission found. - /// No user permission found but open access. - /// Access forbidden. /// Playlist not found. /// /// . /// [HttpGet("{playlistId}/Users/{userId}")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetPlaylistUser( [FromRoute, Required] Guid playlistId, @@ -213,7 +209,12 @@ public class PlaylistsController : BaseJellyfinApiController || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)) || userId.Equals(callingUserId); - return isPermitted ? userPermission is not null ? userPermission : NotFound("User permissions not found") : playlist.OpenAccess ? NoContent() : Forbid(); + if (isPermitted && userPermission is not null) + { + return userPermission; + } + + return NotFound("User permissions not found"); } /// From e3897fe5ddc6013da43557f03b4836e5acfde18c Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 3 Apr 2024 21:20:30 +0200 Subject: [PATCH 042/444] Apply review suggestions --- .../Controllers/PlaylistsController.cs | 21 ++++++++++++------- .../Models/PlaylistDtos/CreatePlaylistDto.cs | 2 +- .../Models/PlaylistDtos/UpdatePlaylistDto.cs | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index ca90d2a6d8..69abe5f7eb 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -94,7 +94,7 @@ public class PlaylistsController : BaseJellyfinApiController UserId = userId.Value, MediaType = mediaType ?? createPlaylistRequest?.MediaType, Users = createPlaylistRequest?.Users.ToArray() ?? [], - Public = createPlaylistRequest?.Public + Public = createPlaylistRequest?.IsPublic }).ConfigureAwait(false); return result; @@ -143,7 +143,7 @@ public class PlaylistsController : BaseJellyfinApiController Name = updatePlaylistRequest.Name, Ids = updatePlaylistRequest.Ids, Users = updatePlaylistRequest.Users, - Public = updatePlaylistRequest.Public + Public = updatePlaylistRequest.IsPublic }).ConfigureAwait(false); return NoContent(); @@ -180,17 +180,19 @@ public class PlaylistsController : BaseJellyfinApiController } /// - /// Get a playlist users. + /// Get a playlist user. /// /// The playlist id. /// The user id. /// User permission found. + /// Access forbidden. /// Playlist not found. /// /// . /// [HttpGet("{playlistId}/Users/{userId}")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetPlaylistUser( [FromRoute, Required] Guid playlistId, @@ -209,7 +211,12 @@ public class PlaylistsController : BaseJellyfinApiController || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)) || userId.Equals(callingUserId); - if (isPermitted && userPermission is not null) + if (!isPermitted) + { + return Forbid(); + } + + if (userPermission is not null) { return userPermission; } @@ -218,7 +225,7 @@ public class PlaylistsController : BaseJellyfinApiController } /// - /// Modify a user to a playlist's users. + /// Modify a user of a playlist's users. /// /// The playlist id. /// The user id. @@ -237,7 +244,7 @@ public class PlaylistsController : BaseJellyfinApiController public async Task UpdatePlaylistUser( [FromRoute, Required] Guid playlistId, [FromRoute, Required] Guid userId, - [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] UpdatePlaylistUserDto updatePlaylistUserRequest) + [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow), Required] UpdatePlaylistUserDto updatePlaylistUserRequest) { var callingUserId = User.GetUserId(); @@ -265,7 +272,7 @@ public class PlaylistsController : BaseJellyfinApiController } /// - /// Remove a user from a playlist's shares. + /// Remove a user from a playlist's users. /// /// The playlist id. /// The user id. diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs index 69694a7699..3cbdd031a1 100644 --- a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs +++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs @@ -41,5 +41,5 @@ public class CreatePlaylistDto /// /// Gets or sets a value indicating whether the playlist is public. /// - public bool Public { get; set; } = true; + public bool IsPublic { get; set; } = true; } diff --git a/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs index 0e109db3ee..80e20995c6 100644 --- a/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs +++ b/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs @@ -30,5 +30,5 @@ public class UpdatePlaylistDto /// /// Gets or sets a value indicating whether the playlist is public. /// - public bool? Public { get; set; } + public bool? IsPublic { get; set; } } From 9031aae6531f86bdf2857badb94308c8a1e82b47 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 3 Apr 2024 21:24:51 +0200 Subject: [PATCH 043/444] Typo --- Jellyfin.Api/Controllers/PlaylistsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 69abe5f7eb..1100f85cf5 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -185,7 +185,7 @@ public class PlaylistsController : BaseJellyfinApiController /// The playlist id. /// The user id. /// User permission found. - /// Access forbidden. + /// Access forbidden. /// Playlist not found. /// /// . From 5eadf6f4cff1dcd607568a7e474fd7ef1b35f482 Mon Sep 17 00:00:00 2001 From: Kristijonas Kuzmickas Date: Thu, 4 Apr 2024 08:06:13 +0000 Subject: [PATCH 044/444] Translated using Weblate (Lithuanian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/ --- Emby.Server.Implementations/Localization/Core/lt-LT.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json index e7279994bb..004ce68f58 100644 --- a/Emby.Server.Implementations/Localization/Core/lt-LT.json +++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json @@ -126,5 +126,7 @@ "External": "Išorinis", "HearingImpaired": "Su klausos sutrikimais", "TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus", - "TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose." + "TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose.", + "TaskCleanCollectionsAndPlaylists": "Sutvarko duomenis jūsų kolekcijose ir grojaraščiuose.", + "TaskCleanCollectionsAndPlaylistsDescription": "Pašalina nebeegzistuojančius elementus iš kolekcijų ir grojaraščių." } From c87c26d33acbe63b623182112bf4ec6ddf0d2bbe Mon Sep 17 00:00:00 2001 From: Tim Zschuppe Date: Thu, 4 Apr 2024 19:02:51 +0000 Subject: [PATCH 045/444] Translated using Weblate (German) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/de/ --- Emby.Server.Implementations/Localization/Core/de.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index 7a4c2067ba..d8b2f828f6 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -126,5 +126,7 @@ "External": "Extern", "HearingImpaired": "Hörgeschädigt", "TaskRefreshTrickplayImages": "Trickplay-Bilder generieren", - "TaskRefreshTrickplayImagesDescription": "Erstellt eine Trickplay-Vorschau für Videos in aktivierten Bibliotheken." + "TaskRefreshTrickplayImagesDescription": "Erstellt eine Trickplay-Vorschau für Videos in aktivierten Bibliotheken.", + "TaskCleanCollectionsAndPlaylists": "Sammlungen und Playlisten aufräumen", + "TaskCleanCollectionsAndPlaylistsDescription": "Lösche nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten." } From ddda30fe23a04e07401bc870ac33213ff8a34c71 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Fri, 5 Apr 2024 21:11:09 +0200 Subject: [PATCH 046/444] Only allow owner and admin to delete playlists --- MediaBrowser.Controller/Entities/BaseItem.cs | 2 +- MediaBrowser.Controller/Playlists/Playlist.cs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index ac9698ec9f..5f9840b1bb 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -833,7 +833,7 @@ namespace MediaBrowser.Controller.Entities return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders); } - public bool CanDelete(User user) + public virtual bool CanDelete(User user) { var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType().ToList(); diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index 747dd9f637..34b34e5780 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -255,6 +255,11 @@ namespace MediaBrowser.Controller.Playlists return shares.Any(s => s.UserId.Equals(userId)); } + public override bool CanDelete(User user) + { + return user.HasPermission(PermissionKind.IsAdministrator) || user.Id.Equals(OwnerUserId); + } + public override bool IsVisibleStandalone(User user) { if (!IsSharedItem) From 3a8e65893283d3f8759170ce72b050e0e632b280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=92=D0=BB?= =?UTF-8?q?=D0=B0=D0=B4=D0=B8=D0=BC=D0=B8=D1=80=D0=BE=D0=B2=D0=B8=D1=87=20?= =?UTF-8?q?=D0=98=D0=BD=D1=8F=D0=BA=D0=B8=D0=BD?= Date: Sun, 7 Apr 2024 11:51:38 +0000 Subject: [PATCH 047/444] 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, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index 26d678a0c3..3d3f88709b 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -126,5 +126,7 @@ "External": "Внешние", "HearingImpaired": "Для слабослышащих", "TaskRefreshTrickplayImages": "Сгенерировать изображения для Trickplay", - "TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена." + "TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена.", + "TaskCleanCollectionsAndPlaylists": "Очистка коллекций и списков воспроизведения", + "TaskCleanCollectionsAndPlaylistsDescription": "Удаляет элементы из коллекций и списков воспроизведения, которые больше не существуют." } From 6b6aab04ceadfd43a6bd0abb416b08006ff1ca6c Mon Sep 17 00:00:00 2001 From: gnattu Date: Mon, 8 Apr 2024 21:42:47 +0800 Subject: [PATCH 048/444] Fix apple audio codecs (#11315) --- .../MediaEncoding/EncodingHelper.cs | 10 +++++++++- MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 250e0143f9..717b53a0b6 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -107,7 +107,6 @@ namespace MediaBrowser.Controller.MediaEncoding { "wmav2", 2 }, { "libmp3lame", 2 }, { "libfdk_aac", 6 }, - { "aac_at", 6 }, { "ac3", 6 }, { "eac3", 6 }, { "dca", 6 }, @@ -752,6 +751,15 @@ namespace MediaBrowser.Controller.MediaEncoding return "dca"; } + if (string.Equals(codec, "alac", StringComparison.OrdinalIgnoreCase)) + { + // The ffmpeg upstream breaks the AudioToolbox ALAC encoder in version 6.1 but fixes it in version 7.0. + // Since ALAC is lossless in quality and the AudioToolbox encoder is not faster, + // its only benefit is a smaller file size. + // To prevent problems, use the ffmpeg native encoder instead. + return "alac"; + } + return codec.ToLowerInvariant(); } diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index ae0284e3ab..5f0779dc7b 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -69,6 +69,7 @@ namespace MediaBrowser.MediaEncoding.Encoder "aac_at", "libfdk_aac", "ac3", + "alac", "dca", "libmp3lame", "libopus", From ab1fd326d55c8e4a942714f615f28c1983bee978 Mon Sep 17 00:00:00 2001 From: Tina Date: Mon, 8 Apr 2024 16:00:48 +0200 Subject: [PATCH 049/444] Add jacket to the list of music images (#11314) --- MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs index 894aebed4b..9aa9c3548d 100644 --- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs @@ -32,6 +32,7 @@ namespace MediaBrowser.LocalMetadata.Images "folder", "poster", "cover", + "jacket", "default" }; From f62671dc3fc4c5edf76887120949ef9ce604dd01 Mon Sep 17 00:00:00 2001 From: bene toffix Date: Mon, 8 Apr 2024 16:01:46 +0000 Subject: [PATCH 050/444] Translated using Weblate (Catalan) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ca/ --- Emby.Server.Implementations/Localization/Core/ca.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index c4d8c69479..b7633f77c3 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -126,5 +126,7 @@ "External": "Extern", "HearingImpaired": "Discapacitat auditiva", "TaskRefreshTrickplayImages": "Generar miniatures de línia de temps", - "TaskRefreshTrickplayImagesDescription": "Crear miniatures de línia de temps per vídeos en les biblioteques habilitades." + "TaskRefreshTrickplayImagesDescription": "Crear miniatures de línia de temps per vídeos en les biblioteques habilitades.", + "TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.", + "TaskCleanCollectionsAndPlaylists": "Neteja col·leccions i llistes de reproducció" } From acf77169a0b1c750f3dd908df1487c5b88c5ef02 Mon Sep 17 00:00:00 2001 From: milo !! Date: Mon, 8 Apr 2024 11:26:14 +0000 Subject: [PATCH 051/444] Translated using Weblate (English (United Kingdom)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/en_GB/ --- Emby.Server.Implementations/Localization/Core/en-GB.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json index 32bf893100..ff0c3d23df 100644 --- a/Emby.Server.Implementations/Localization/Core/en-GB.json +++ b/Emby.Server.Implementations/Localization/Core/en-GB.json @@ -126,5 +126,7 @@ "External": "External", "HearingImpaired": "Hearing Impaired", "TaskRefreshTrickplayImages": "Generate Trickplay Images", - "TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries." + "TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries.", + "TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists", + "TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist." } From 3d7d0297fe2b10241704c303bc5e9f99467ee501 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Mon, 8 Apr 2024 22:24:24 +0200 Subject: [PATCH 052/444] Fix policy for GetRemoteSubtitles Other operations related to remote subtitles require the SubtitleManagement policy, so it only makes sense that this operation requires it too. --- Jellyfin.Api/Controllers/SubtitleController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index cc2a630e1d..e2c5486d98 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -165,7 +165,7 @@ public class SubtitleController : BaseJellyfinApiController /// File returned. /// A with the subtitle file. [HttpGet("Providers/Subtitles/Subtitles/{subtitleId}")] - [Authorize] + [Authorize(Policy = Policies.SubtitleManagement)] [ProducesResponseType(StatusCodes.Status200OK)] [Produces(MediaTypeNames.Application.Octet)] [ProducesFile("text/*")] From ee4a782ed44dcba58fbf08b7e1fa3789801fda19 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 14:47:08 -0600 Subject: [PATCH 053/444] chore(deps): update dependency svg.skia to v1.0.0.18 (#11319) 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 308d40f33f..9367d397d9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -73,7 +73,7 @@ - + From 00620a4092412c885eed393a81de460affe3bbb3 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Mon, 8 Apr 2024 16:52:10 -0400 Subject: [PATCH 054/444] Fix disabled libraries being returned in MediaFolders api (#11236) --- Jellyfin.Api/Controllers/LibraryController.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 984dc77896..360389d292 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -520,7 +520,11 @@ public class LibraryController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetMediaFolders([FromQuery] bool? isHidden) { - var items = _libraryManager.GetUserRootFolder().Children.Concat(_libraryManager.RootFolder.VirtualChildren).OrderBy(i => i.SortName).ToList(); + var items = _libraryManager.GetUserRootFolder().Children + .Concat(_libraryManager.RootFolder.VirtualChildren) + .Where(i => _libraryManager.GetLibraryOptions(i).Enabled) + .OrderBy(i => i.SortName) + .ToList(); if (isHidden.HasValue) { From e6873de7daa1782a621892f13c844f59785aee01 Mon Sep 17 00:00:00 2001 From: Blackspirits Date: Tue, 9 Apr 2024 11:07:27 +0000 Subject: [PATCH 055/444] Translated using Weblate (Portuguese (Portugal)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/ --- Emby.Server.Implementations/Localization/Core/pt-PT.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index 92ac2681e4..dc96088ff5 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -126,5 +126,7 @@ "External": "Externo", "HearingImpaired": "Surdo", "TaskRefreshTrickplayImages": "Gerar imagens de truques", - "TaskRefreshTrickplayImagesDescription": "Cria vizualizações de truques para videos nas librarias ativas." + "TaskRefreshTrickplayImagesDescription": "Cria vizualizações de truques para videos nas librarias ativas.", + "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.", + "TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução" } From 63d7d84d2c3338155511cafc44c64084649bf02a Mon Sep 17 00:00:00 2001 From: Blackspirits Date: Tue, 9 Apr 2024 11:07:20 +0000 Subject: [PATCH 056/444] Translated using Weblate (Portuguese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt/ --- Emby.Server.Implementations/Localization/Core/pt.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index 103393a1e4..de487488e3 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -125,5 +125,7 @@ "TaskKeyframeExtractor": "Extrator de quadro-chave", "TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo.", "TaskRefreshTrickplayImages": "Gerar miniaturas de vídeo", - "TaskRefreshTrickplayImagesDescription": "Cria miniaturas de vídeo para vídeos nas bibliotecas definidas." + "TaskRefreshTrickplayImagesDescription": "Cria miniaturas de vídeo para vídeos nas bibliotecas definidas.", + "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.", + "TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução" } From 574ad0c7fa7de80855e42b9ee10387cc8c79f02c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Apr 2024 11:25:54 -0600 Subject: [PATCH 057/444] chore(deps): update dotnet monorepo to v8.0.4 (#11328) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- Directory.Packages.props | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index c6670e9f58..8e82e30018 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "8.0.3", + "version": "8.0.4", "commands": [ "dotnet-ef" ] diff --git a/Directory.Packages.props b/Directory.Packages.props index 9367d397d9..78ff47ea85 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,15 +25,15 @@ - + - + - - - - - + + + + + @@ -42,8 +42,8 @@ - - + + From f7f7ba885383d164239976a79e8af4b698aa8bdd Mon Sep 17 00:00:00 2001 From: ViggoC Date: Wed, 10 Apr 2024 16:07:57 +0000 Subject: [PATCH 058/444] Translated using Weblate (Chinese (Simplified)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hans/ --- Emby.Server.Implementations/Localization/Core/zh-CN.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index b88d4eeaf5..1f1458b6c0 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -126,5 +126,7 @@ "External": "外部", "HearingImpaired": "听力障碍", "TaskRefreshTrickplayImages": "生成时间轴缩略图", - "TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成时间轴缩略图。" + "TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成时间轴缩略图。", + "TaskCleanCollectionsAndPlaylists": "清理合集和播放列表", + "TaskCleanCollectionsAndPlaylistsDescription": "清理合集和播放列表中已不存在的项目。" } From e93fa27e4c1404220524d33d2034275344e55a85 Mon Sep 17 00:00:00 2001 From: GeorgeH005 <72687949+GeorgeH005@users.noreply.github.com> Date: Wed, 10 Apr 2024 22:33:24 +0300 Subject: [PATCH 059/444] Add support for out-of-spec but existent, Dolby Vision Profile 8 CCid 6 media. (#11334) --- MediaBrowser.Model/Entities/MediaStream.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index a620bc9b54..0d2d7c6965 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -730,6 +730,8 @@ namespace MediaBrowser.Model.Entities 1 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10), 4 => (VideoRange.HDR, VideoRangeType.DOVIWithHLG), 2 => (VideoRange.SDR, VideoRangeType.DOVIWithSDR), + // While not in Dolby Spec, Profile 8 CCid 6 media are possible to create, and since CCid 6 stems from Bluray (Profile 7 originally) an HDR10 base layer is guaranteed to exist. + 6 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10), // There is no other case to handle here as per Dolby Spec. Default case included for completeness and linting purposes _ => (VideoRange.SDR, VideoRangeType.SDR) }, From 92eb9e3a94b3923172f1b0f5ea7b7dacd21ead5b Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Wed, 10 Apr 2024 22:32:37 -0600 Subject: [PATCH 060/444] Always grant access for Administrator role --- .../FirstTimeSetupHandler.cs | 40 ++----------------- .../FirstTimeSetupHandlerTests.cs | 12 ++++++ 2 files changed, 16 insertions(+), 36 deletions(-) diff --git a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs index 965b7e7e60..2b6b2a82c4 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs @@ -1,10 +1,6 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; -using Jellyfin.Api.Extensions; -using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy @@ -15,19 +11,14 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy public class FirstTimeSetupHandler : AuthorizationHandler { private readonly IConfigurationManager _configurationManager; - private readonly IUserManager _userManager; /// /// Initializes a new instance of the class. /// /// Instance of the interface. - /// Instance of the interface. - public FirstTimeSetupHandler( - IConfigurationManager configurationManager, - IUserManager userManager) + public FirstTimeSetupHandler(IConfigurationManager configurationManager) { _configurationManager = configurationManager; - _userManager = userManager; } /// @@ -36,37 +27,14 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted) { context.Succeed(requirement); - return Task.CompletedTask; } - - var contextUser = context.User; - if (requirement.RequireAdmin && !contextUser.IsInRole(UserRoles.Administrator)) + else if (requirement.RequireAdmin && !context.User.IsInRole(UserRoles.Administrator)) { context.Fail(); - return Task.CompletedTask; } - - var userId = contextUser.GetUserId(); - if (userId.IsEmpty()) - { - context.Fail(); - return Task.CompletedTask; - } - - if (!requirement.ValidateParentalSchedule) - { - context.Succeed(requirement); - return Task.CompletedTask; - } - - var user = _userManager.GetUserById(userId); - if (user is null) - { - throw new ResourceNotFoundException(); - } - - if (user.IsParentalScheduleAllowed()) + else { + // Any user-specific checks are handled in the DefaultAuthorizationHandler. context.Succeed(requirement); } diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs index 1ea1797ba1..3687d77534 100644 --- a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Security.Claims; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; @@ -67,5 +68,16 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupPolicy await _firstTimeSetupHandler.HandleAsync(context); Assert.Equal(shouldSucceed, context.HasSucceeded); } + + [Fact] + public async Task ShouldAllowAdminApiKeyIfStartupWizardComplete() + { + TestHelpers.SetupConfigurationManager(_configurationManagerMock, true); + var claims = new ClaimsPrincipal(new ClaimsIdentity([new Claim(ClaimTypes.Role, UserRoles.Administrator)])); + var context = new AuthorizationHandlerContext(_requirements, claims, null); + + await _firstTimeSetupHandler.HandleAsync(context); + Assert.True(context.HasSucceeded); + } } } From 0c36f539ec23129758501613216b9898e6286371 Mon Sep 17 00:00:00 2001 From: Dan Johansen Date: Thu, 11 Apr 2024 10:33:04 +0000 Subject: [PATCH 061/444] Translated using Weblate (Danish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/da/ --- Emby.Server.Implementations/Localization/Core/da.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index 092af34b6b..b5e2c9b6b8 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -126,5 +126,7 @@ "External": "Ekstern", "HearingImpaired": "Hørehæmmet", "TaskRefreshTrickplayImages": "Generér Trickplay Billeder", - "TaskRefreshTrickplayImagesDescription": "Laver trickplay forhåndsvisninger for videoer i aktiverede biblioteker." + "TaskRefreshTrickplayImagesDescription": "Laver trickplay forhåndsvisninger for videoer i aktiverede biblioteker.", + "TaskCleanCollectionsAndPlaylists": "Ryd op i samlinger og afspilningslister", + "TaskCleanCollectionsAndPlaylistsDescription": "Fjerner enheder fra samlinger og afspilningslister der ikke eksisterer længere." } From 6f1cf595b8d479d37356423f67fd27ed03bd6ba2 Mon Sep 17 00:00:00 2001 From: VitoFe Date: Thu, 11 Apr 2024 16:05:26 +0000 Subject: [PATCH 062/444] Translated using Weblate (Italian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/ --- Emby.Server.Implementations/Localization/Core/it.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index a34bcc4907..8d8311557e 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -126,5 +126,7 @@ "External": "Esterno", "HearingImpaired": "con problemi di udito", "TaskRefreshTrickplayImages": "Genera immagini Trickplay", - "TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate." + "TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate.", + "TaskCleanCollectionsAndPlaylists": "Ripulire le raccolte e le playlist", + "TaskCleanCollectionsAndPlaylistsDescription": "Rimuove gli elementi dalle raccolte e dalle playlist che non esistono più." } From cbd8472478252a2777ede89ff6acf34384326c65 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 Apr 2024 00:43:48 +0000 Subject: [PATCH 063/444] chore(deps): update xunit-dotnet monorepo --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 78ff47ea85..cc4964357a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -85,8 +85,8 @@ - + - + \ No newline at end of file From f8266b3e088b235300920ececf3569ab87e669a5 Mon Sep 17 00:00:00 2001 From: hoanghuy309 Date: Fri, 12 Apr 2024 09:28:56 +0000 Subject: [PATCH 064/444] Translated using Weblate (Vietnamese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/vi/ --- Emby.Server.Implementations/Localization/Core/vi.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json index e92752c5f7..af9b54ad1e 100644 --- a/Emby.Server.Implementations/Localization/Core/vi.json +++ b/Emby.Server.Implementations/Localization/Core/vi.json @@ -125,5 +125,7 @@ "External": "Bên ngoài", "HearingImpaired": "Khiếm Thính", "TaskRefreshTrickplayImages": "Tạo Ảnh Xem Trước Trickplay", - "TaskRefreshTrickplayImagesDescription": "Tạo bản xem trước trịckplay cho video trong thư viện đã bật." + "TaskRefreshTrickplayImagesDescription": "Tạo bản xem trước trịckplay cho video trong thư viện đã bật.", + "TaskCleanCollectionsAndPlaylists": "Dọn dẹp bộ sưu tập và danh sách phát", + "TaskCleanCollectionsAndPlaylistsDescription": "Xóa các mục khỏi bộ sưu tập và danh sách phát không còn tồn tại." } From d3b9ebfa2ee7917e74863c33170873ae4c3dbc44 Mon Sep 17 00:00:00 2001 From: Mikal Stordal Date: Fri, 12 Apr 2024 22:53:39 +0000 Subject: [PATCH 065/444] fix: fix off-by-one error in `GetAttributeValue` Co-authored-by: fearnlj01 --- Emby.Server.Implementations/Library/PathExtensions.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs index c4b6b37561..21e7079d88 100644 --- a/Emby.Server.Implementations/Library/PathExtensions.cs +++ b/Emby.Server.Implementations/Library/PathExtensions.cs @@ -31,8 +31,9 @@ namespace Emby.Server.Implementations.Library var attributeIndex = str.IndexOf(attribute, StringComparison.OrdinalIgnoreCase); - // Must be at least 3 characters after the attribute =, ], any character. - var maxIndex = str.Length - attribute.Length - 3; + // Must be at least 3 characters after the attribute =, ], any character, + // then we offset it by 1, because we want the index and not length. + var maxIndex = str.Length - attribute.Length - 2; while (attributeIndex > -1 && attributeIndex < maxIndex) { var attributeEnd = attributeIndex + attribute.Length; From eccc9a0b647b8cdd7380ea83ddbb77333ce691e3 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Fri, 12 Apr 2024 17:44:16 -0600 Subject: [PATCH 066/444] Add index for lastPlayedDate (#11342) --- Emby.Server.Implementations/Data/SqliteUserDataRepository.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index a5edcc58c0..20359e4ad7 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -58,7 +58,8 @@ namespace Emby.Server.Implementations.Data "create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)", "create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)", "create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)", - "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)")); + "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)", + "create index if not exists UserDatasIndex5 on UserDatas (key, userId, lastPlayedDate)")); if (!userDataTableExists) { From ab731d92128f7068771434f3e157495e3d3f66e5 Mon Sep 17 00:00:00 2001 From: Dominik Krivohlavek Date: Sat, 13 Apr 2024 01:44:30 +0200 Subject: [PATCH 067/444] Fix track MBID in audio metadata (#11301) --- .../MediaInfo/AudioFileProber.cs | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index c9fe4c9b64..67b84681de 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -407,7 +407,14 @@ namespace MediaBrowser.Providers.MediaInfo if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out _)) { - audio.SetProviderId(MetadataProvider.MusicBrainzTrack, tags.MusicBrainzTrackId); + // Fallback to ffprobe as TagLib incorrectly provides recording MBID in `tags.MusicBrainzTrackId`. + // See https://github.com/mono/taglib-sharp/issues/304 + var mediaInfo = await GetMediaInfo(audio, CancellationToken.None).ConfigureAwait(false); + var trackMbId = mediaInfo.GetProviderId(MetadataProvider.MusicBrainzTrack); + if (trackMbId is not null) + { + audio.SetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId); + } } // Save extracted lyrics if they exist, @@ -431,5 +438,20 @@ namespace MediaBrowser.Providers.MediaInfo audio.LyricFiles = externalLyricFiles.Select(i => i.Path).Distinct().ToArray(); currentStreams.AddRange(externalLyricFiles); } + + private async Task GetMediaInfo(BaseItem item, CancellationToken cancellationToken) + { + var request = new MediaInfoRequest + { + MediaType = DlnaProfileType.Audio, + MediaSource = new MediaSourceInfo + { + Path = item.Path, + Protocol = item.PathProtocol ?? MediaProtocol.File + } + }; + + return await _mediaEncoder.GetMediaInfo(request, cancellationToken).ConfigureAwait(false); + } } } From 134bf7a6a58402a08e8e59f7f9ee626881dfa183 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sat, 13 Apr 2024 01:44:45 +0200 Subject: [PATCH 068/444] Don't throw if file was already removed (#11286) --- .../Library/LibraryManager.cs | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 0c854bdb74..dced6868d7 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -338,7 +338,7 @@ namespace Emby.Server.Implementations.Library if (item is LiveTvProgram) { _logger.LogDebug( - "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", + "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", item.GetType().Name, item.Name ?? "Unknown name", item.Path ?? string.Empty, @@ -347,7 +347,7 @@ namespace Emby.Server.Implementations.Library else { _logger.LogInformation( - "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", + "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", item.GetType().Name, item.Name ?? "Unknown name", item.Path ?? string.Empty, @@ -366,7 +366,7 @@ namespace Emby.Server.Implementations.Library } _logger.LogDebug( - "Deleting metadata path, Type: {0}, Name: {1}, Path: {2}, Id: {3}", + "Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", item.GetType().Name, item.Name ?? "Unknown name", metadataPath, @@ -395,7 +395,7 @@ namespace Emby.Server.Implementations.Library try { _logger.LogInformation( - "Deleting item path, Type: {0}, Name: {1}, Path: {2}, Id: {3}", + "Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", item.GetType().Name, item.Name ?? "Unknown name", fileSystemInfo.FullName, @@ -410,6 +410,24 @@ namespace Emby.Server.Implementations.Library File.Delete(fileSystemInfo.FullName); } } + catch (DirectoryNotFoundException) + { + _logger.LogInformation( + "Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", + item.GetType().Name, + item.Name ?? "Unknown name", + fileSystemInfo.FullName, + item.Id); + } + catch (FileNotFoundException) + { + _logger.LogInformation( + "File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", + item.GetType().Name, + item.Name ?? "Unknown name", + fileSystemInfo.FullName, + item.Id); + } catch (IOException) { if (isRequiredForDelete) From 7d28d08e08a412ab88ede368220562799f2bd7c0 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Sat, 13 Apr 2024 01:45:01 +0200 Subject: [PATCH 069/444] Enable more warnings as errors (#11288) --- Emby.Server.Implementations/ApplicationHost.cs | 14 +++++++------- .../Data/BaseSqliteRepository.cs | 5 +---- Emby.Server.Implementations/Dto/DtoService.cs | 7 ++++--- .../Library/LibraryManager.cs | 2 +- .../Library/ResolverHelper.cs | 2 +- .../Library/Resolvers/Movies/BoxSetResolver.cs | 2 +- .../Session/SessionManager.cs | 5 +---- .../MediaEncoding/EncodingHelper.cs | 10 +++++----- jellyfin.ruleset | 6 ++++++ 9 files changed, 27 insertions(+), 26 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index acabbb059b..6add7e0b39 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -109,13 +109,13 @@ namespace Emby.Server.Implementations /// /// The disposable parts. /// - private readonly ConcurrentDictionary _disposableParts = new(); + private readonly ConcurrentBag _disposableParts = new(); private readonly DeviceId _deviceId; private readonly IConfiguration _startupConfig; private readonly IXmlSerializer _xmlSerializer; private readonly IStartupOptions _startupOptions; - private readonly IPluginManager _pluginManager; + private readonly PluginManager _pluginManager; private List _creatingInstances; @@ -161,7 +161,7 @@ namespace Emby.Server.Implementations ApplicationPaths.PluginsPath, ApplicationVersion); - _disposableParts.TryAdd((PluginManager)_pluginManager, byte.MinValue); + _disposableParts.Add(_pluginManager); } /// @@ -360,7 +360,7 @@ namespace Emby.Server.Implementations { foreach (var part in parts.OfType()) { - _disposableParts.TryAdd(part, byte.MinValue); + _disposableParts.Add(part); } } @@ -381,7 +381,7 @@ namespace Emby.Server.Implementations { foreach (var part in parts.OfType()) { - _disposableParts.TryAdd(part, byte.MinValue); + _disposableParts.Add(part); } } @@ -457,7 +457,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(ConfigurationManager); serviceCollection.AddSingleton(ConfigurationManager); serviceCollection.AddSingleton(this); - serviceCollection.AddSingleton(_pluginManager); + serviceCollection.AddSingleton(_pluginManager); serviceCollection.AddSingleton(ApplicationPaths); serviceCollection.AddSingleton(); @@ -965,7 +965,7 @@ namespace Emby.Server.Implementations Logger.LogInformation("Disposing {Type}", type.Name); - foreach (var (part, _) in _disposableParts) + foreach (var part in _disposableParts.ToArray()) { var partType = part.GetType(); if (partType == type) diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index bf079d90ca..b1c99227c3 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -186,10 +186,7 @@ namespace Emby.Server.Implementations.Data protected void CheckDisposed() { - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name, "Object has been disposed and cannot be accessed."); - } + ObjectDisposedException.ThrowIf(_disposed, this); } /// diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 5da9bea262..98eacb52b2 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -668,12 +668,13 @@ namespace Emby.Server.Implementations.Dto { dto.ImageBlurHashes ??= new Dictionary>(); - if (!dto.ImageBlurHashes.ContainsKey(image.Type)) + if (!dto.ImageBlurHashes.TryGetValue(image.Type, out var value)) { - dto.ImageBlurHashes[image.Type] = new Dictionary(); + value = new Dictionary(); + dto.ImageBlurHashes[image.Type] = value; } - dto.ImageBlurHashes[image.Type][tag] = image.BlurHash; + value[tag] = image.BlurHash; } return tag; diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index dced6868d7..bb5cc746e9 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -461,7 +461,7 @@ namespace Emby.Server.Implementations.Library ReportItemRemoved(item, parent); } - private static IEnumerable GetMetadataPaths(BaseItem item, IEnumerable children) + private static List GetMetadataPaths(BaseItem item, IEnumerable children) { var list = new List { diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs index 7a61e2607c..52be76217e 100644 --- a/Emby.Server.Implementations/Library/ResolverHelper.cs +++ b/Emby.Server.Implementations/Library/ResolverHelper.cs @@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.Library item.Id = libraryManager.GetNewItemId(item.Path, item.GetType()); - item.IsLocked = item.Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1 || + item.IsLocked = item.Path.Contains("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) || item.GetParents().Any(i => i.IsLocked); // Make sure DateCreated and DateModified have values diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs index 6cc04ea810..955055313e 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs @@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return null; } - if (filename.IndexOf("[boxset]", StringComparison.OrdinalIgnoreCase) != -1 || args.ContainsFileSystemEntryByName("collection.xml")) + if (filename.Contains("[boxset]", StringComparison.OrdinalIgnoreCase) || args.ContainsFileSystemEntryByName("collection.xml")) { return new BoxSet { diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 75945b08a2..06798628f3 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -159,10 +159,7 @@ namespace Emby.Server.Implementations.Session private void CheckDisposed() { - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } + ObjectDisposedException.ThrowIf(_disposed, this); } private void OnSessionStarted(SessionInfo info) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 717b53a0b6..eb375c8a25 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1271,23 +1271,23 @@ namespace MediaBrowser.Controller.MediaEncoding { var codec = stream.Codec ?? string.Empty; - return codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1 - || codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1; + return codec.Contains("264", StringComparison.OrdinalIgnoreCase) + || codec.Contains("avc", StringComparison.OrdinalIgnoreCase); } public static bool IsH265(MediaStream stream) { var codec = stream.Codec ?? string.Empty; - return codec.IndexOf("265", StringComparison.OrdinalIgnoreCase) != -1 - || codec.IndexOf("hevc", StringComparison.OrdinalIgnoreCase) != -1; + return codec.Contains("265", StringComparison.OrdinalIgnoreCase) + || codec.Contains("hevc", StringComparison.OrdinalIgnoreCase); } public static bool IsAAC(MediaStream stream) { var codec = stream.Codec ?? string.Empty; - return codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1; + return codec.Contains("aac", StringComparison.OrdinalIgnoreCase); } public static string GetBitStreamArgs(MediaStream stream) diff --git a/jellyfin.ruleset b/jellyfin.ruleset index 10225e3af8..db116f46c8 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -85,6 +85,8 @@ + + @@ -101,6 +103,8 @@ + + @@ -108,6 +112,8 @@ + + From 31e0756c0c39f01de09a1b59145918c43488bcbf Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Fri, 12 Apr 2024 17:45:15 -0600 Subject: [PATCH 070/444] Only update if actively refreshing (#11341) --- MediaBrowser.Controller/Entities/Folder.cs | 20 ++----------------- .../Manager/ProviderManager.cs | 17 +++++++--------- 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index a2957cdca4..8bfcf5deef 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -460,15 +460,7 @@ namespace MediaBrowser.Controller.Entities progress.Report(percent); - // TODO: this is sometimes being called after the refresh has completed. - try - { - ProviderManager.OnRefreshProgress(folder, percent); - } - catch (InvalidOperationException e) - { - Logger.LogError(e, "Error refreshing folder"); - } + ProviderManager.OnRefreshProgress(folder, percent); }); if (validChildrenNeedGeneration) @@ -500,15 +492,7 @@ namespace MediaBrowser.Controller.Entities if (recursive) { - // TODO: this is sometimes being called after the refresh has completed. - try - { - ProviderManager.OnRefreshProgress(folder, percent); - } - catch (InvalidOperationException e) - { - Logger.LogError(e, "Error refreshing folder"); - } + ProviderManager.OnRefreshProgress(folder, percent); } }); diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index a9ebf7ec72..0b1fed0a3d 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -968,16 +968,13 @@ namespace MediaBrowser.Providers.Manager var id = item.Id; _logger.LogDebug("OnRefreshProgress {Id:N} {Progress}", id, progress); - // TODO: Need to hunt down the conditions for this happening - _activeRefreshes.AddOrUpdate( - id, - _ => throw new InvalidOperationException( - string.Format( - CultureInfo.InvariantCulture, - "Cannot update refresh progress of item '{0}' ({1}) because a refresh for this item is not running", - item.GetType().Name, - item.Id.ToString("N", CultureInfo.InvariantCulture))), - (_, _) => progress); + if (!_activeRefreshes.TryGetValue(id, out var current) + || progress <= current + || !_activeRefreshes.TryUpdate(id, progress, current)) + { + // Item isn't currently refreshing, or update was received out-of-order, so don't trigger event. + return; + } try { From 33367c1e390e8905ce297377c9402c969ea439e3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 Apr 2024 17:45:42 -0600 Subject: [PATCH 071/444] chore(deps): update github/codeql-action action to v3.24.10 (#11304) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci-codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 7b76e47f56..39fe6f1d26 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '8.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + uses: github/codeql-action/init@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # v3.24.10 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + uses: github/codeql-action/autobuild@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # v3.24.10 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + uses: github/codeql-action/analyze@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # v3.24.10 From c566ccb63bf61f9c36743ddb2108a57c65a2519b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 Apr 2024 17:45:50 -0600 Subject: [PATCH 072/444] chore(deps): update skiasharp monorepo (#11337) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index cc4964357a..d67dba2250 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,7 +18,7 @@ - + @@ -68,9 +68,9 @@ - - - + + + From 204146a3a504c4cc417c9eb68c32271e3f585352 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sat, 13 Apr 2024 14:48:40 +0800 Subject: [PATCH 073/444] fix: mark UserRoot as non-root when performing removal Fixes #11269 Signed-off-by: gnattu --- Emby.Server.Implementations/Library/LibraryManager.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index bb5cc746e9..baed887e3e 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1033,7 +1033,7 @@ namespace Emby.Server.Implementations.Library } } - private async Task ValidateTopLibraryFolders(CancellationToken cancellationToken) + private async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false) { await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); @@ -1046,11 +1046,15 @@ namespace Emby.Server.Implementations.Library await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false); + // HACK: override IsRootHere for libraries to be removed + if (removeRoot) GetUserRootFolder().IsRoot = false; await GetUserRootFolder().ValidateChildren( new Progress(), new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: false, cancellationToken).ConfigureAwait(false); + // HACK: restore IsRoot here after validation + if (removeRoot) GetUserRootFolder().IsRoot = true; // Quickly scan CollectionFolders for changes foreach (var folder in GetUserRootFolder().Children.OfType()) @@ -3118,7 +3122,7 @@ namespace Emby.Server.Implementations.Library if (refreshLibrary) { - await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false); + await ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false); StartScanInBackground(); } From 4fa6b8874f0cc773a977b09aec249d207a1335d3 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sat, 13 Apr 2024 14:58:29 +0800 Subject: [PATCH 074/444] fix: typo Signed-off-by: gnattu --- Emby.Server.Implementations/Library/LibraryManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index baed887e3e..da6c756743 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1046,7 +1046,7 @@ namespace Emby.Server.Implementations.Library await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false); - // HACK: override IsRootHere for libraries to be removed + // HACK: override IsRoot here for libraries to be removed if (removeRoot) GetUserRootFolder().IsRoot = false; await GetUserRootFolder().ValidateChildren( new Progress(), From 7befbda1a66ed0a0c0f386081e13c0b2585b8137 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sat, 13 Apr 2024 15:02:13 +0800 Subject: [PATCH 075/444] fix: code style Signed-off-by: gnattu --- Emby.Server.Implementations/Library/LibraryManager.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index da6c756743..8f5f366883 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1047,14 +1047,21 @@ namespace Emby.Server.Implementations.Library await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false); // HACK: override IsRoot here for libraries to be removed - if (removeRoot) GetUserRootFolder().IsRoot = false; + if (removeRoot) + { + GetUserRootFolder().IsRoot = false; + } + await GetUserRootFolder().ValidateChildren( new Progress(), new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: false, cancellationToken).ConfigureAwait(false); // HACK: restore IsRoot here after validation - if (removeRoot) GetUserRootFolder().IsRoot = true; + if (removeRoot) + { + GetUserRootFolder().IsRoot = true; + } // Quickly scan CollectionFolders for changes foreach (var folder in GetUserRootFolder().Children.OfType()) From 730a75a88a850bdd53dcacaccb43385e654b5ec8 Mon Sep 17 00:00:00 2001 From: Jordan Fearnley Date: Sat, 13 Apr 2024 02:04:40 +0100 Subject: [PATCH 076/444] Chore: Adds unit tests to support (#11351) --- .../Library/PathExtensionsTests.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs index d1be07aa22..940e3c2b12 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs @@ -18,6 +18,12 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("Superman: Red Son [tmdbid=618355][imdbid=tt10985510]", "imdbid", "tt10985510")] [InlineData("Superman: Red Son [tmdbid-618355][imdbid-tt10985510]", "imdbid", "tt10985510")] [InlineData("Superman: Red Son [tmdbid-618355][imdbid-tt10985510]", "tmdbid", "618355")] + [InlineData("Superman: Red Son [providera-id=1]", "providera-id", "1")] + [InlineData("Superman: Red Son [providerb-id=2]", "providerb-id", "2")] + [InlineData("Superman: Red Son [providera id=4]", "providera id", "4")] + [InlineData("Superman: Red Son [providerb id=5]", "providerb id", "5")] + [InlineData("Superman: Red Son [tmdbid=3]", "tmdbid", "3")] + [InlineData("Superman: Red Son [tvdbid-6]", "tvdbid", "6")] [InlineData("[tmdbid=618355]", "tmdbid", "618355")] [InlineData("[tmdbid-618355]", "tmdbid", "618355")] [InlineData("tmdbid=111111][tmdbid=618355]", "tmdbid", "618355")] From 22f9ea580ce2c126930b95e09ba4b4a7d840511d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 13 Apr 2024 10:23:33 +0000 Subject: [PATCH 077/444] chore(deps): update dependency libse to v4 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index d67dba2250..10635cd643 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -22,7 +22,7 @@ - + From 8026202d7e3ea08fd81a1404b08d07b9b83f4dcc Mon Sep 17 00:00:00 2001 From: Moe Ye Htet Date: Sun, 14 Apr 2024 02:56:46 +0000 Subject: [PATCH 078/444] Translated using Weblate (Burmese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/my/ --- .../Localization/Core/my.json | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/my.json b/Emby.Server.Implementations/Localization/Core/my.json index 198f7540c8..4cb4cdc757 100644 --- a/Emby.Server.Implementations/Localization/Core/my.json +++ b/Emby.Server.Implementations/Localization/Core/my.json @@ -48,7 +48,7 @@ "Undefined": "သတ်မှတ်မထားသော", "TvShows": "တီဗီ ဇာတ်လမ်းတွဲများ", "System": "စနစ်", - "Sync": "ထပ်တူကျသည်။", + "Sync": "ချိန်ကိုက်မည်", "SubtitleDownloadFailureFromForItem": "{1} အတွက် {0} မှ စာတန်းထိုးများ ဒေါင်းလုဒ်လုပ်ခြင်း မအောင်မြင်ပါ", "StartupEmbyServerIsLoading": "Jellyfin ဆာဗာကို အသင့်ပြင်နေပါသည်။ ခဏနေ ထပ်စမ်းကြည့်ပါ။", "Songs": "သီချင်းများ", @@ -104,7 +104,7 @@ "HeaderFavoriteSongs": "အကြိုက်ဆုံးသီချင်းများ", "HeaderFavoriteShows": "အကြိုက်ဆုံး ဇာတ်လမ်းတွဲများ", "HeaderFavoriteEpisodes": "အကြိုက်ဆုံး ဇာတ်လမ်းအပိုင်းများ", - "HeaderFavoriteArtists": "အကြိုက်ဆုံးအနုပညာရှင်များ", + "HeaderFavoriteArtists": "အကြိုက်ဆုံး အနုပညာရှင်များ", "HeaderFavoriteAlbums": "အကြိုက်ဆုံး အယ်လ်ဘမ်များ", "HeaderContinueWatching": "ဆက်လက်ကြည့်ရှုပါ", "HeaderAlbumArtists": "အယ်လ်ဘမ်အနုပညာရှင်များ", @@ -120,5 +120,11 @@ "AuthenticationSucceededWithUserName": "{0} အောင်မြင်စွာ စစ်မှန်ကြောင်း အတည်ပြုပြီးပါပြီ", "Application": "အပလီကေးရှင်း", "AppDeviceValues": "အက်ပ်- {0}၊ စက်- {1}", - "External": "ပြင်ပ" + "External": "ပြင်ပ", + "TaskKeyframeExtractorDescription": "ပိုမိုတိကျသည့် အိတ်ချ်အယ်လ်အက်စ် အစဉ်လိုက်ပြသမှုများ ဖန်တီးနိုင်ရန်အတွက် ဗီဒီယိုဖိုင်များမှ ကီးဖရိန်များကို ထုတ်နှုတ်ယူမည် ဖြစ်သည်။ ဤလုပ်ဆောင်မှုသည် အချိန်ကြာရှည်နိုင်သည်။", + "TaskCleanCollectionsAndPlaylistsDescription": "စုစည်းမှုများနှင့် အစဉ်လိုက်ပြသမှုများမှ မရှိတော့သည်များကို ဖယ်ရှားမည်။", + "TaskRefreshTrickplayImages": "ထရစ်ခ်ပလေး ပုံများကို ထုတ်မည်", + "TaskKeyframeExtractor": "ကီးဖရိန်များကို ထုတ်နုတ်ခြင်း", + "TaskCleanCollectionsAndPlaylists": "စုစည်းမှုများနှင့် အစဉ်လိုက်ပြသမှုများကို ရှင်းလင်းမည်", + "HearingImpaired": "အကြားအာရုံ ချို့တဲ့သူ" } From 453a5bdcf309a8943b510299d1a497433a8aa070 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 14 Apr 2024 07:57:53 -0600 Subject: [PATCH 079/444] chore(deps): update eps1lon/actions-label-merge-conflict action to v3 (#11200) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/pull-request-conflict.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request-conflict.yml b/.github/workflows/pull-request-conflict.yml index 05517bb030..1c3fac3c6e 100644 --- a/.github/workflows/pull-request-conflict.yml +++ b/.github/workflows/pull-request-conflict.yml @@ -15,7 +15,7 @@ jobs: if: ${{ github.repository == 'jellyfin/jellyfin' }} steps: - name: Apply label - uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0 + uses: eps1lon/actions-label-merge-conflict@e62d7a53ff8be8b97684bffb6cfbbf3fc1115e2e # v3.0.0 if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}} with: dirtyLabel: 'merge conflict' From 9a4db8008593647cb6728b10317680dd3152c934 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 14 Apr 2024 08:18:09 -0600 Subject: [PATCH 080/444] chore(deps): update dependency efcoresecondlevelcacheinterceptor to v4.4.1 (#11306) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Cody Robibero --- Directory.Packages.props | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 10635cd643..e125b536a7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,7 +16,7 @@ - + diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs index bb8d4dd14f..3d747f2ea9 100644 --- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs +++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs @@ -22,11 +22,9 @@ public static class ServiceCollectionExtensions serviceCollection.AddEFSecondLevelCache(options => options.UseMemoryCacheProvider() .CacheAllQueries(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(10)) - .DisableLogging(true) .UseCacheKeyPrefix("EF_") // Don't cache null values. Remove this optional setting if it's not necessary. - .SkipCachingResults(result => - result.Value is null || (result.Value is EFTableRows rows && rows.RowsCount == 0))); + .SkipCachingResults(result => result.Value is null or EFTableRows { RowsCount: 0 })); serviceCollection.AddPooledDbContextFactory((serviceProvider, opt) => { From 6fb6b5f1766a1f37a61b9faaa40209bab995bf30 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Sun, 14 Apr 2024 08:18:36 -0600 Subject: [PATCH 081/444] Validate item access (#11171) --- .../Library/LibraryManager.cs | 39 +++++- .../DisplayPreferencesController.cs | 2 +- Jellyfin.Api/Controllers/FilterController.cs | 2 +- Jellyfin.Api/Controllers/ImageController.cs | 20 +-- .../Controllers/InstantMixController.cs | 60 +++++++-- .../Controllers/ItemLookupController.cs | 13 +- .../Controllers/ItemRefreshController.cs | 5 +- .../Controllers/ItemUpdateController.cs | 12 +- Jellyfin.Api/Controllers/ItemsController.cs | 12 +- Jellyfin.Api/Controllers/LibraryController.cs | 69 +++++------ .../Controllers/LibraryStructureController.cs | 12 +- Jellyfin.Api/Controllers/LiveTvController.cs | 27 +++- Jellyfin.Api/Controllers/LyricsController.cs | 58 +++------ .../Controllers/MediaInfoController.cs | 42 +++++-- .../Controllers/PlaylistsController.cs | 7 +- .../Controllers/PlaystateController.cs | 21 ++-- .../Controllers/RemoteImageController.cs | 9 +- Jellyfin.Api/Controllers/SearchController.cs | 2 +- .../Controllers/SubtitleController.cs | 50 ++++++-- .../Controllers/TrickplayController.cs | 4 +- Jellyfin.Api/Controllers/TvShowsController.cs | 15 +-- .../Controllers/UniversalAudioController.cs | 27 +++- .../Controllers/UserLibraryController.cs | 116 ++++-------------- .../Controllers/VideoAttachmentsController.cs | 5 +- Jellyfin.Api/Controllers/VideosController.cs | 43 ++++--- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 14 +-- Jellyfin.Api/Helpers/StreamingHelpers.cs | 5 +- .../Library/ILibraryManager.cs | 20 +++ 28 files changed, 422 insertions(+), 289 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index bb5cc746e9..0a4432beca 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -46,6 +46,7 @@ using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; +using TMDbLib.Objects.Authentication; using Episode = MediaBrowser.Controller.Entities.TV.Episode; using EpisodeInfo = Emby.Naming.TV.EpisodeInfo; using Genre = MediaBrowser.Controller.Entities.Genre; @@ -1222,12 +1223,7 @@ namespace Emby.Server.Implementations.Library return null; } - /// - /// Gets the item by id. - /// - /// The id. - /// BaseItem. - /// is null. + /// public BaseItem GetItemById(Guid id) { if (id.IsEmpty()) @@ -1263,6 +1259,22 @@ namespace Emby.Server.Implementations.Library return null; } + /// + public T GetItemById(Guid id, Guid userId) + where T : BaseItem + { + var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId); + return GetItemById(id, user); + } + + /// + public T GetItemById(Guid id, User user) + where T : BaseItem + { + var item = GetItemById(id); + return ItemIsVisible(item, user) ? item : null; + } + public List GetItemList(InternalItemsQuery query, bool allowExternalContent) { if (query.Recursive && !query.ParentId.IsEmpty()) @@ -3191,5 +3203,20 @@ namespace Emby.Server.Implementations.Library CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions); } + + private static bool ItemIsVisible(BaseItem item, User user) + { + if (item is null) + { + return false; + } + + if (user is null) + { + return true; + } + + return item is UserRootFolder || item.IsVisibleStandalone(user); + } } } diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 1cad663264..6d94d96f3a 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -194,7 +194,7 @@ public class DisplayPreferencesController : BaseJellyfinApiController foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase))) { - if (!Enum.TryParse(displayPreferences.CustomPrefs[key], true, out var type)) + if (!Enum.TryParse(displayPreferences.CustomPrefs[key], true, out _)) { _logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]); displayPreferences.CustomPrefs.Remove(key); diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index d6e043e6a1..4abca32713 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -162,7 +162,7 @@ public class FilterController : BaseJellyfinApiController } else if (parentId.HasValue) { - parentItem = _libraryManager.GetItemById(parentId.Value); + parentItem = _libraryManager.GetItemById(parentId.Value); } var filters = new QueryFilters(); diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 6b38fa7d34..8e8accab3c 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -90,6 +90,7 @@ public class ImageController : BaseJellyfinApiController /// User Id. /// Image updated. /// User does not have permission to delete the image. + /// Item not found. /// A . [HttpPost("UserImage")] [Authorize] @@ -97,6 +98,7 @@ public class ImageController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task PostUserImage( [FromQuery] Guid? userId) { @@ -289,7 +291,7 @@ public class ImageController : BaseJellyfinApiController [FromRoute, Required] ImageType imageType, [FromQuery] int? imageIndex) { - var item = _libraryManager.GetItemById(itemId); + var item = _libraryManager.GetItemById(itemId, User.GetUserId()); if (item is null) { return NotFound(); @@ -317,7 +319,7 @@ public class ImageController : BaseJellyfinApiController [FromRoute, Required] ImageType imageType, [FromRoute] int imageIndex) { - var item = _libraryManager.GetItemById(itemId); + var item = _libraryManager.GetItemById(itemId, User.GetUserId()); if (item is null) { return NotFound(); @@ -346,7 +348,7 @@ public class ImageController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromRoute, Required] ImageType imageType) { - var item = _libraryManager.GetItemById(itemId); + var item = _libraryManager.GetItemById(itemId, User.GetUserId()); if (item is null) { return NotFound(); @@ -390,7 +392,7 @@ public class ImageController : BaseJellyfinApiController [FromRoute, Required] ImageType imageType, [FromRoute] int imageIndex) { - var item = _libraryManager.GetItemById(itemId); + var item = _libraryManager.GetItemById(itemId, User.GetUserId()); if (item is null) { return NotFound(); @@ -433,7 +435,7 @@ public class ImageController : BaseJellyfinApiController [FromRoute, Required] int imageIndex, [FromQuery, Required] int newIndex) { - var item = _libraryManager.GetItemById(itemId); + var item = _libraryManager.GetItemById(itemId, User.GetUserId()); if (item is null) { return NotFound(); @@ -456,7 +458,7 @@ public class ImageController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetItemImageInfos([FromRoute, Required] Guid itemId) { - var item = _libraryManager.GetItemById(itemId); + var item = _libraryManager.GetItemById(itemId, User.GetUserId()); if (item is null) { return NotFound(); @@ -559,7 +561,7 @@ public class ImageController : BaseJellyfinApiController [FromQuery] string? foregroundLayer, [FromQuery] int? imageIndex) { - var item = _libraryManager.GetItemById(itemId); + var item = _libraryManager.GetItemById(itemId, User.GetUserId()); if (item is null) { return NotFound(); @@ -637,7 +639,7 @@ public class ImageController : BaseJellyfinApiController [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer) { - var item = _libraryManager.GetItemById(itemId); + var item = _libraryManager.GetItemById(itemId, User.GetUserId()); if (item is null) { return NotFound(); @@ -715,7 +717,7 @@ public class ImageController : BaseJellyfinApiController [FromQuery] string? foregroundLayer, [FromRoute, Required] int imageIndex) { - var item = _libraryManager.GetItemById(itemId); + var item = _libraryManager.GetItemById(itemId, User.GetUserId()); if (item is null) { return NotFound(); diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index 3cf4852995..dcbacf1d78 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -62,9 +62,11 @@ public class InstantMixController : BaseJellyfinApiController /// Optional. The max number of images to return, per image type. /// Optional. The image types to include in the output. /// Instant playlist returned. + /// Item not found. /// A with the playlist items. [HttpGet("Songs/{itemId}/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetInstantMixFromSong( [FromRoute, Required] Guid itemId, [FromQuery] Guid? userId, @@ -75,11 +77,16 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { - var item = _libraryManager.GetItemById(itemId); userId = RequestHelpers.GetUserId(User, userId); var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); + var item = _libraryManager.GetItemById(itemId, user); + if (item is null) + { + return NotFound(); + } + var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -99,9 +106,11 @@ public class InstantMixController : BaseJellyfinApiController /// Optional. The max number of images to return, per image type. /// Optional. The image types to include in the output. /// Instant playlist returned. + /// Item not found. /// A with the playlist items. [HttpGet("Albums/{itemId}/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetInstantMixFromAlbum( [FromRoute, Required] Guid itemId, [FromQuery] Guid? userId, @@ -112,15 +121,20 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { - var album = _libraryManager.GetItemById(itemId); userId = RequestHelpers.GetUserId(User, userId); var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); + var item = _libraryManager.GetItemById(itemId, user); + if (item is null) + { + return NotFound(); + } + var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions); + var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); return GetResult(items, user, limit, dtoOptions); } @@ -136,9 +150,11 @@ public class InstantMixController : BaseJellyfinApiController /// Optional. The max number of images to return, per image type. /// Optional. The image types to include in the output. /// Instant playlist returned. + /// Item not found. /// A with the playlist items. [HttpGet("Playlists/{itemId}/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetInstantMixFromPlaylist( [FromRoute, Required] Guid itemId, [FromQuery] Guid? userId, @@ -149,15 +165,20 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { - var playlist = (Playlist)_libraryManager.GetItemById(itemId); userId = RequestHelpers.GetUserId(User, userId); var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); + var item = _libraryManager.GetItemById(itemId, user); + if (item is null) + { + return NotFound(); + } + var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions); + var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); return GetResult(items, user, limit, dtoOptions); } @@ -209,9 +230,11 @@ public class InstantMixController : BaseJellyfinApiController /// Optional. The max number of images to return, per image type. /// Optional. The image types to include in the output. /// Instant playlist returned. + /// Item not found. /// A with the playlist items. [HttpGet("Artists/{itemId}/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetInstantMixFromArtists( [FromRoute, Required] Guid itemId, [FromQuery] Guid? userId, @@ -222,11 +245,16 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { - var item = _libraryManager.GetItemById(itemId); userId = RequestHelpers.GetUserId(User, userId); var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); + var item = _libraryManager.GetItemById(itemId, user); + if (item is null) + { + return NotFound(); + } + var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -246,9 +274,11 @@ public class InstantMixController : BaseJellyfinApiController /// Optional. The max number of images to return, per image type. /// Optional. The image types to include in the output. /// Instant playlist returned. + /// Item not found. /// A with the playlist items. [HttpGet("Items/{itemId}/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetInstantMixFromItem( [FromRoute, Required] Guid itemId, [FromQuery] Guid? userId, @@ -259,11 +289,16 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { - var item = _libraryManager.GetItemById(itemId); userId = RequestHelpers.GetUserId(User, userId); var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); + var item = _libraryManager.GetItemById(itemId, user); + if (item is null) + { + return NotFound(); + } + var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -283,9 +318,11 @@ public class InstantMixController : BaseJellyfinApiController /// Optional. The max number of images to return, per image type. /// Optional. The image types to include in the output. /// Instant playlist returned. + /// Item not found. /// A with the playlist items. [HttpGet("Artists/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] [Obsolete("Use GetInstantMixFromArtists")] public ActionResult> GetInstantMixFromArtists2( [FromQuery, Required] Guid id, @@ -320,9 +357,11 @@ public class InstantMixController : BaseJellyfinApiController /// Optional. The max number of images to return, per image type. /// Optional. The image types to include in the output. /// Instant playlist returned. + /// Item not found. /// A with the playlist items. [HttpGet("MusicGenres/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetInstantMixFromMusicGenreById( [FromQuery, Required] Guid id, [FromQuery] Guid? userId, @@ -333,11 +372,16 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { - var item = _libraryManager.GetItemById(id); userId = RequestHelpers.GetUserId(User, userId); var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); + var item = _libraryManager.GetItemById(id, user); + if (item is null) + { + return NotFound(); + } + var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs index e3aee1bf7a..d009f80a96 100644 --- a/Jellyfin.Api/Controllers/ItemLookupController.cs +++ b/Jellyfin.Api/Controllers/ItemLookupController.cs @@ -4,6 +4,8 @@ using System.ComponentModel.DataAnnotations; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using MediaBrowser.Common.Api; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -64,7 +66,7 @@ public class ItemLookupController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetExternalIdInfos([FromRoute, Required] Guid itemId) { - var item = _libraryManager.GetItemById(itemId); + var item = _libraryManager.GetItemById(itemId, User.GetUserId()); if (item is null) { return NotFound(); @@ -234,6 +236,7 @@ public class ItemLookupController : BaseJellyfinApiController /// The remote search result. /// Optional. Whether or not to replace all images. Default: True. /// Item metadata refreshed. + /// Item not found. /// /// A that represents the asynchronous operation to get the remote search results. /// The task result contains an . @@ -241,12 +244,18 @@ public class ItemLookupController : BaseJellyfinApiController [HttpPost("Items/RemoteSearch/Apply/{itemId}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task ApplySearchCriteria( [FromRoute, Required] Guid itemId, [FromBody, Required] RemoteSearchResult searchResult, [FromQuery] bool replaceAllImages = true) { - var item = _libraryManager.GetItemById(itemId); + var item = _libraryManager.GetItemById(itemId, User.GetUserId()); + if (item is null) + { + return NotFound(); + } + _logger.LogInformation( "Setting provider id's to item {ItemId}-{ItemName}: {@ProviderIds}", item.Id, diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs index 0a8522e1cf..c1343b1309 100644 --- a/Jellyfin.Api/Controllers/ItemRefreshController.cs +++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs @@ -2,7 +2,10 @@ using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using MediaBrowser.Common.Api; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; @@ -61,7 +64,7 @@ public class ItemRefreshController : BaseJellyfinApiController [FromQuery] bool replaceAllMetadata = false, [FromQuery] bool replaceAllImages = false) { - var item = _libraryManager.GetItemById(itemId); + var item = _libraryManager.GetItemById(itemId, User.GetUserId()); if (item is null) { return NotFound(); diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index 9800248c68..83f308bb19 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -5,6 +5,8 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; using MediaBrowser.Common.Api; using MediaBrowser.Controller.Configuration; @@ -72,7 +74,7 @@ public class ItemUpdateController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task UpdateItem([FromRoute, Required] Guid itemId, [FromBody, Required] BaseItemDto request) { - var item = _libraryManager.GetItemById(itemId); + var item = _libraryManager.GetItemById(itemId, User.GetUserId()); if (item is null) { return NotFound(); @@ -145,7 +147,11 @@ public class ItemUpdateController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetMetadataEditorInfo([FromRoute, Required] Guid itemId) { - var item = _libraryManager.GetItemById(itemId); + var item = _libraryManager.GetItemById(itemId, User.GetUserId()); + if (item is null) + { + return NotFound(); + } var info = new MetadataEditorInfo { @@ -197,7 +203,7 @@ public class ItemUpdateController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string? contentType) { - var item = _libraryManager.GetItemById(itemId); + var item = _libraryManager.GetItemById(itemId, User.GetUserId()); if (item is null) { return NotFound(); diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 26ae1a820f..6ffe6e7da1 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -967,9 +967,13 @@ public class ItemsController : BaseJellyfinApiController } var user = _userManager.GetUserById(requestUserId) ?? throw new ResourceNotFoundException(); - var item = _libraryManager.GetItemById(itemId); + var item = _libraryManager.GetItemById(itemId, user); + if (item is null) + { + return NotFound(); + } - return (item == null) ? NotFound() : _userDataRepository.GetUserDataDto(item, user); + return _userDataRepository.GetUserDataDto(item, user); } /// @@ -1014,8 +1018,8 @@ public class ItemsController : BaseJellyfinApiController } var user = _userManager.GetUserById(requestUserId) ?? throw new ResourceNotFoundException(); - var item = _libraryManager.GetItemById(itemId); - if (item == null) + var item = _libraryManager.GetItemById(itemId, user); + if (item is null) { return NotFound(); } diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 360389d292..3b4e80ff3c 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -102,7 +102,7 @@ public class LibraryController : BaseJellyfinApiController [ProducesFile("video/*", "audio/*")] public ActionResult GetFile([FromRoute, Required] Guid itemId) { - var item = _libraryManager.GetItemById(itemId); + var item = _libraryManager.GetItemById(itemId, User.GetUserId()); if (item is null) { return NotFound(); @@ -152,11 +152,10 @@ public class LibraryController : BaseJellyfinApiController ? (userId.IsNullOrEmpty() ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder()) - : _libraryManager.GetItemById(itemId); - + : _libraryManager.GetItemById(itemId, user); if (item is null) { - return NotFound("Item not found."); + return NotFound(); } IEnumerable themeItems; @@ -214,16 +213,14 @@ public class LibraryController : BaseJellyfinApiController var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); - var item = itemId.IsEmpty() ? (userId.IsNullOrEmpty() ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder()) - : _libraryManager.GetItemById(itemId); - + : _libraryManager.GetItemById(itemId, user); if (item is null) { - return NotFound("Item not found."); + return NotFound(); } IEnumerable themeItems; @@ -286,7 +283,8 @@ public class LibraryController : BaseJellyfinApiController userId, inheritFromParent); - if (themeSongs.Result is NotFoundObjectResult || themeVideos.Result is NotFoundObjectResult) + if (themeSongs.Result is StatusCodeResult { StatusCode: StatusCodes.Status404NotFound } + || themeVideos.Result is StatusCodeResult { StatusCode: StatusCodes.Status404NotFound }) { return NotFound(); } @@ -327,6 +325,7 @@ public class LibraryController : BaseJellyfinApiController /// The item id. /// Item deleted. /// Unauthorized access. + /// Item not found. /// A . [HttpDelete("Items/{itemId}")] [Authorize] @@ -335,17 +334,18 @@ public class LibraryController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult DeleteItem(Guid itemId) { - var isApiKey = User.GetIsApiKey(); var userId = User.GetUserId(); - var user = !isApiKey && !userId.IsEmpty() - ? _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException() - : null; - if (!isApiKey && user is null) + var isApiKey = User.GetIsApiKey(); + var user = userId.IsEmpty() && isApiKey + ? null + : _userManager.GetUserById(userId); + + if (user is null && !isApiKey) { - return Unauthorized("Unauthorized access"); + return NotFound(); } - var item = _libraryManager.GetItemById(itemId); + var item = _libraryManager.GetItemById(itemId, user); if (item is null) { return NotFound(); @@ -391,7 +391,7 @@ public class LibraryController : BaseJellyfinApiController foreach (var i in ids) { - var item = _libraryManager.GetItemById(i); + var item = _libraryManager.GetItemById(i, user); if (item is null) { return NotFound(); @@ -459,19 +459,17 @@ public class LibraryController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId) { - var item = _libraryManager.GetItemById(itemId); userId = RequestHelpers.GetUserId(User, userId); - - if (item is null) - { - return NotFound("Item not found"); - } - - var baseItemDtos = new List(); - var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); + var item = _libraryManager.GetItemById(itemId, user); + if (item is null) + { + return NotFound(); + } + + var baseItemDtos = new List(); var dtoOptions = new DtoOptions().AddClientFields(User); BaseItem? parent = item.GetParent(); @@ -644,14 +642,16 @@ public class LibraryController : BaseJellyfinApiController [ProducesFile("video/*", "audio/*")] public async Task GetDownload([FromRoute, Required] Guid itemId) { - var item = _libraryManager.GetItemById(itemId); + var userId = User.GetUserId(); + var user = userId.IsEmpty() + ? null + : _userManager.GetUserById(userId); + var item = _libraryManager.GetItemById(itemId, user); if (item is null) { return NotFound(); } - var user = _userManager.GetUserById(User.GetUserId()); - if (user is not null) { if (!item.CanDownload(user)) @@ -704,12 +704,14 @@ public class LibraryController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) { userId = RequestHelpers.GetUserId(User, userId); + var user = userId.IsNullOrEmpty() + ? null + : _userManager.GetUserById(userId.Value); var item = itemId.IsEmpty() - ? (userId.IsNullOrEmpty() + ? (user is null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder()) - : _libraryManager.GetItemById(itemId); - + : _libraryManager.GetItemById(itemId, user); if (item is null) { return NotFound(); @@ -720,9 +722,6 @@ public class LibraryController : BaseJellyfinApiController return new QueryResult(); } - var user = userId.IsNullOrEmpty() - ? null - : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User); diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index 23c430f859..c1d01a5c2f 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -6,6 +6,8 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LibraryStructureDto; using MediaBrowser.Common.Api; @@ -311,15 +313,21 @@ public class LibraryStructureController : BaseJellyfinApiController /// /// The library name and options. /// Library updated. + /// Item not found. /// A . [HttpPost("LibraryOptions")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateLibraryOptions( [FromBody] UpdateLibraryOptionsDto request) { - var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id); + var item = _libraryManager.GetItemById(request.Id, User.GetUserId()); + if (item is null) + { + return NotFound(); + } - collectionFolder.UpdateLibraryOptions(request.LibraryOptions); + item.UpdateLibraryOptions(request.LibraryOptions); return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 7768b3c45f..2b26c01f88 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -220,9 +220,11 @@ public class LiveTvController : BaseJellyfinApiController /// Channel id. /// Optional. Attach user data. /// Live tv channel returned. + /// Item not found. /// An containing the live tv channel. [HttpGet("Channels/{channelId}")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] [Authorize(Policy = Policies.LiveTvAccess)] public ActionResult GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId) { @@ -232,7 +234,12 @@ public class LiveTvController : BaseJellyfinApiController : _userManager.GetUserById(userId.Value); var item = channelId.IsEmpty() ? _libraryManager.GetUserRootFolder() - : _libraryManager.GetItemById(channelId); + : _libraryManager.GetItemById(channelId, user); + + if (item is null) + { + return NotFound(); + } var dtoOptions = new DtoOptions() .AddClientFields(User); @@ -416,9 +423,11 @@ public class LiveTvController : BaseJellyfinApiController /// Recording id. /// Optional. Attach user data. /// Recording returned. + /// Item not found. /// An containing the live tv recording. [HttpGet("Recordings/{recordingId}")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] [Authorize(Policy = Policies.LiveTvAccess)] public ActionResult GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId) { @@ -426,7 +435,13 @@ public class LiveTvController : BaseJellyfinApiController var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); - var item = recordingId.IsEmpty() ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId); + var item = recordingId.IsEmpty() + ? _libraryManager.GetUserRootFolder() + : _libraryManager.GetItemById(recordingId, user); + if (item is null) + { + return NotFound(); + } var dtoOptions = new DtoOptions() .AddClientFields(User); @@ -611,7 +626,8 @@ public class LiveTvController : BaseJellyfinApiController { query.IsSeries = true; - if (_libraryManager.GetItemById(librarySeriesId.Value) is Series series) + var series = _libraryManager.GetItemById(librarySeriesId.Value); + if (series is not null) { query.Name = series.Name; } @@ -665,7 +681,8 @@ public class LiveTvController : BaseJellyfinApiController { query.IsSeries = true; - if (_libraryManager.GetItemById(body.LibrarySeriesId) is Series series) + var series = _libraryManager.GetItemById(body.LibrarySeriesId); + if (series is not null) { query.Name = series.Name; } @@ -779,7 +796,7 @@ public class LiveTvController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId) { - var item = _libraryManager.GetItemById(recordingId); + var item = _libraryManager.GetItemById(recordingId, User.GetUserId()); if (item is null) { return NotFound(); diff --git a/Jellyfin.Api/Controllers/LyricsController.cs b/Jellyfin.Api/Controllers/LyricsController.cs index f2b312b478..8eb4cadf88 100644 --- a/Jellyfin.Api/Controllers/LyricsController.cs +++ b/Jellyfin.Api/Controllers/LyricsController.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Extensions; using MediaBrowser.Common.Api; using MediaBrowser.Controller.Entities.Audio; @@ -66,37 +67,16 @@ public class LyricsController : BaseJellyfinApiController [HttpGet("Audio/{itemId}/Lyrics")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetLyrics([FromRoute, Required] Guid itemId) { - var isApiKey = User.GetIsApiKey(); - var userId = User.GetUserId(); - if (!isApiKey && userId.IsEmpty()) - { - return BadRequest(); - } - - var audio = _libraryManager.GetItemById