From 88b3490d1756236d0c2fc00243420d45d149a5d1 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 26 Mar 2024 15:29:48 +0100 Subject: Add playlist ACL endpoints --- Jellyfin.Api/Controllers/PlaylistsController.cs | 122 ++++++++++++++++++++++++ 1 file changed, 122 insertions(+) (limited to 'Jellyfin.Api/Controllers/PlaylistsController.cs') diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 0e7c3f155..f0e8227fd 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. /// -- cgit v1.2.3 From f1dc1610a28fdb2dec4241d233503b6e11020546 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 26 Mar 2024 16:13:07 +0100 Subject: Extend playlist creation capabilities --- Emby.Server.Implementations/Playlists/PlaylistManager.cs | 10 +++------- Jellyfin.Api/Controllers/PlaylistsController.cs | 4 +++- Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs | 13 ++++++++++++- MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs | 7 ++++++- 4 files changed, 24 insertions(+), 10 deletions(-) (limited to 'Jellyfin.Api/Controllers/PlaylistsController.cs') diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 6724d54d1..6b169db79 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 f0e8227fd..bf618e8fd 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 bdc488871..a82bff65e 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 62d496d04..93eccd5c7 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; } } -- cgit v1.2.3 From 56c432a8439e1b75c15729bfdb19d41d34e3124d Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 26 Mar 2024 23:45:14 +0100 Subject: Apply review suggestions --- .../Library/Resolvers/PlaylistResolver.cs | 9 ++-- .../Playlists/PlaylistManager.cs | 14 +++--- Jellyfin.Api/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 -------- MediaBrowser.Model/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 (limited to 'Jellyfin.Api/Controllers/PlaylistsController.cs') diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs index a50435ae6..a03c1214d 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 6b169db79..59c96852a 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 bf618e8fd..c38061c7d 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 a82bff65e..6eedd2131 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 aaca1cc49..238923d29 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 9a08a4ce3..dfd9b8330 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 8a870e0d9..4ee1b2ef6 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 31574a3ff..fb6b6e424 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 186aad189..000000000 --- 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 000000000..271feed11 --- /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 93eccd5c7..f1351588f 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; } -- cgit v1.2.3 From 2aaa9f669a0ba6855329ce63ae4e785dc70a5a69 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 27 Mar 2024 06:39:14 +0100 Subject: Apply review suggestions --- Jellyfin.Api/Controllers/PlaylistsController.cs | 73 +++++++++++++++++++------ MediaBrowser.Model/Entities/UserPermissions.cs | 19 +++++-- 2 files changed, 69 insertions(+), 23 deletions(-) (limited to 'Jellyfin.Api/Controllers/PlaylistsController.cs') diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index c38061c7d..7ca04d7ba 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 271feed11..80e2cd32c 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; } } -- cgit v1.2.3 From bff37ed13aa9ee0267ee5e1248339c6044fa1b0c Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Mon, 1 Apr 2024 19:59:48 +0200 Subject: 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 +- .../Entities/PlaylistUserPermissions.cs | 30 ++++++++++++++++++++++ MediaBrowser.Model/Entities/UserPermissions.cs | 28 -------------------- .../Playlists/PlaylistCreationRequest.cs | 2 +- 10 files changed, 47 insertions(+), 45 deletions(-) create mode 100644 MediaBrowser.Model/Entities/PlaylistUserPermissions.cs delete mode 100644 MediaBrowser.Model/Entities/UserPermissions.cs (limited to 'Jellyfin.Api/Controllers/PlaylistsController.cs') diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 59c96852a..0e5add635 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 7ca04d7ba..862e5235e 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 6eedd2131..69694a769 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 238923d29..1750be619 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 dfd9b8330..b948d2e18 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 4ee1b2ef6..22ae3f12b 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 fb6b6e424..8c4ba6c42 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/PlaylistUserPermissions.cs b/MediaBrowser.Model/Entities/PlaylistUserPermissions.cs new file mode 100644 index 000000000..b5f017d2b --- /dev/null +++ b/MediaBrowser.Model/Entities/PlaylistUserPermissions.cs @@ -0,0 +1,30 @@ +using System; + +namespace MediaBrowser.Model.Entities; + +/// +/// Class to hold data on user permissions for playlists. +/// +public class PlaylistUserPermissions +{ + /// + /// Initializes a new instance of the class. + /// + /// The user id. + /// Edit permission. + public PlaylistUserPermissions(Guid userId, bool canEdit = false) + { + UserId = userId; + CanEdit = canEdit; + } + + /// + /// Gets or sets the user id. + /// + public Guid 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 deleted file mode 100644 index 80e2cd32c..000000000 --- a/MediaBrowser.Model/Entities/UserPermissions.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace MediaBrowser.Model.Entities; - -/// -/// Class to hold data on user permissions for lists. -/// -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; } - - /// - /// Gets or sets a value indicating whether the user has edit permissions. - /// - public bool CanEdit { get; set; } -} diff --git a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs index f1351588f..ec54b1afd 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. -- cgit v1.2.3 From c1dbb49315f90bf03445a960eb8eace86f1ea6f2 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Mon, 1 Apr 2024 20:43:05 +0200 Subject: Implement update endpoint --- .../Playlists/PlaylistManager.cs | 88 ++++++++++++++-------- Jellyfin.Api/Controllers/PlaylistsController.cs | 74 ++++++++++-------- .../Models/PlaylistDtos/UpdatePlaylistDto.cs | 34 +++++++++ .../Migrations/Routines/FixPlaylistOwner.cs | 4 +- .../Playlists/IPlaylistManager.cs | 29 ++++--- MediaBrowser.Controller/Playlists/Playlist.cs | 2 +- .../Parsers/BaseItemXmlParser.cs | 4 +- MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs | 19 ++--- .../Playlists/PlaylistUpdateRequest.cs | 41 ++++++++++ 9 files changed, 202 insertions(+), 93 deletions(-) create mode 100644 Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs create mode 100644 MediaBrowser.Model/Playlists/PlaylistUpdateRequest.cs (limited to 'Jellyfin.Api/Controllers/PlaylistsController.cs') diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 0e5add635..517ec68dc 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); + + 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 UpdatePlaylist(playlist).ConfigureAwait(false); + 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 862e5235e..de4c542d9 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -101,72 +101,82 @@ public class PlaylistsController : BaseJellyfinApiController } /// - /// Get a playlist's users. + /// Updates a playlist. /// /// The playlist id. - /// Found shares. + /// The id. + /// Playlist updated. /// Unauthorized access. /// Playlist not found. /// - /// A list of objects. + /// A that represents the asynchronous operation to update a playlist. + /// The task result contains an indicating success. /// - [HttpGet("{playlistId}/User")] + [HttpPost("{playlistId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult> GetPlaylistUsers( - [FromRoute, Required] Guid playlistId) + public async Task UpdatePlaylist( + [FromRoute, Required] Guid playlistId, + [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Disallow)] UpdatePlaylistDto updatePlaylistRequest) { - var userId = User.GetUserId(); + var callingUserId = User.GetUserId(); - var playlist = _playlistManager.GetPlaylist(userId, playlistId); + var playlist = _playlistManager.GetPlaylist(callingUserId, 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)); + var isPermitted = playlist.OwnerUserId.Equals(callingUserId) + || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)); - return isPermitted ? playlist.Shares.ToList() : Unauthorized("Unauthorized Access"); + 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(); } /// - /// Toggles public access of a playlist. + /// Get a playlist's users. /// /// The playlist id. - /// Public access toggled. + /// Found shares. /// 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. + /// A list of objects. /// - [HttpPost("{playlistId}/TogglePublic")] - [ProducesResponseType(StatusCodes.Status204NoContent)] + [HttpGet("{playlistId}/User")] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task TogglePublicAccess( + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult> GetPlaylistUsers( [FromRoute, Required] Guid playlistId) { - var callingUserId = User.GetUserId(); + var userId = User.GetUserId(); - var playlist = _playlistManager.GetPlaylist(callingUserId, playlistId); + var playlist = _playlistManager.GetPlaylist(userId, 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); + var isPermitted = playlist.OwnerUserId.Equals(userId) + || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(userId)); - return NoContent(); + return isPermitted ? playlist.Shares.ToList() : Unauthorized("Unauthorized Access"); } /// @@ -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 000000000..93e544eed --- /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 06596c171..3655a610d 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 1750be619..464620427 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -20,19 +20,25 @@ namespace MediaBrowser.Controller.Playlists Playlist GetPlaylist(Guid userId, Guid playlistId); /// - /// Gets the playlists. + /// Creates the playlist. /// - /// The user identifier. - /// IEnumerable<Playlist>. - IEnumerable GetPlaylists(Guid userId); + /// The . + /// Task<Playlist>. + Task CreatePlaylist(PlaylistCreationRequest request); /// - /// Toggle OpenAccess policy of the playlist. + /// Updates a playlist. /// - /// The playlist identifier. - /// The user identifier. + /// The . /// Task. - Task ToggleOpenAccess(Guid playlistId, Guid userId); + Task UpdatePlaylist(PlaylistUpdateRequest request); + + /// + /// Gets the playlists. + /// + /// The user identifier. + /// IEnumerable<Playlist>. + IEnumerable GetPlaylists(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 b948d2e18..747dd9f63 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 22ae3f12b..a7e027d94 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 5a7193079..ee0d10bea 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 000000000..f574e679c --- /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; } +} -- cgit v1.2.3 From 8cf77424f6c432386fb06017eccfa0e9f880a3ba Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 2 Apr 2024 08:08:36 +0200 Subject: Apply review suggestions --- .../Playlists/PlaylistManager.cs | 24 +++++++++-------- Jellyfin.Api/Controllers/PlaylistsController.cs | 31 +++++++++++++--------- .../Models/PlaylistDtos/UpdatePlaylistDto.cs | 2 +- .../Models/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 (limited to 'Jellyfin.Api/Controllers/PlaylistsController.cs') diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 517ec68dc..7a6cf9eff 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 de4c542d9..12186e02e 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 93e544eed..0e109db3e 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 000000000..60467b5e7 --- /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 464620427..821b901a0 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 f574e679c..db290bbdb 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 000000000..1840efdf3 --- /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; } +} -- cgit v1.2.3 From 3e0b201688d6efeca5c65df11425001f73c8c9ec Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 3 Apr 2024 16:06:20 +0200 Subject: Enforce permissions --- Jellyfin.Api/Controllers/PlaylistsController.cs | 105 +++++++++++++++++---- .../Playlists/IPlaylistManager.cs | 2 +- 2 files changed, 88 insertions(+), 19 deletions(-) (limited to 'Jellyfin.Api/Controllers/PlaylistsController.cs') diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 12186e02e..4ced64ae7 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 821b901a0..cbe4bd87f 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); /// -- cgit v1.2.3 From 04c5b9d6800d7790d08958b9d6700299ba0c8e74 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 3 Apr 2024 16:14:06 +0200 Subject: Add endpoint to get user permissions --- Jellyfin.Api/Controllers/PlaylistsController.cs | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) (limited to 'Jellyfin.Api/Controllers/PlaylistsController.cs') diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 4ced64ae7..567a27412 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. /// -- cgit v1.2.3 From d72f40fe4159d83440c3362d137901e4c418c4b9 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 3 Apr 2024 16:19:13 +0200 Subject: Return 204 on OpenAccess --- Jellyfin.Api/Controllers/PlaylistsController.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'Jellyfin.Api/Controllers/PlaylistsController.cs') diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 567a27412..d6239503c 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(); } /// -- cgit v1.2.3 From 247ec19de4945f4d7ab64d4a8772b72759e3d2b7 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 3 Apr 2024 16:23:14 +0200 Subject: Fixup --- Jellyfin.Api/Controllers/PlaylistsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Jellyfin.Api/Controllers/PlaylistsController.cs') diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index d6239503c..54b2261ab 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); -- cgit v1.2.3 From 5396b616bf2d7e387bc20539eeae03e841f60c01 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 3 Apr 2024 16:32:25 +0200 Subject: Fixup --- Jellyfin.Api/Controllers/PlaylistsController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'Jellyfin.Api/Controllers/PlaylistsController.cs') diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 54b2261ab..01c1c578d 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(); } /// -- cgit v1.2.3 From 3c7562313b3d0a8bdaaa6623215f2c979150cf5f Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 3 Apr 2024 16:57:10 +0200 Subject: Apply review suggestions --- Jellyfin.Api/Controllers/PlaylistsController.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'Jellyfin.Api/Controllers/PlaylistsController.cs') diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 01c1c578d..d167d996c 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)] -- cgit v1.2.3 From 51e2faa448624a334128042cdca1e592ec97240e Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 3 Apr 2024 20:06:57 +0200 Subject: Apply review suggestions --- Jellyfin.Api/Controllers/PlaylistsController.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'Jellyfin.Api/Controllers/PlaylistsController.cs') diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index d167d996c..ca90d2a6d 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"); } /// -- cgit v1.2.3 From e3897fe5ddc6013da43557f03b4836e5acfde18c Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 3 Apr 2024 21:20:30 +0200 Subject: Apply review suggestions --- Jellyfin.Api/Controllers/PlaylistsController.cs | 21 ++++++++++++++------- .../Models/PlaylistDtos/CreatePlaylistDto.cs | 2 +- .../Models/PlaylistDtos/UpdatePlaylistDto.cs | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) (limited to 'Jellyfin.Api/Controllers/PlaylistsController.cs') diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index ca90d2a6d..69abe5f7e 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 69694a769..3cbdd031a 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 0e109db3e..80e20995c 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; } } -- cgit v1.2.3 From 9031aae6531f86bdf2857badb94308c8a1e82b47 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 3 Apr 2024 21:24:51 +0200 Subject: Typo --- Jellyfin.Api/Controllers/PlaylistsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Jellyfin.Api/Controllers/PlaylistsController.cs') diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 69abe5f7e..1100f85cf 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. /// /// . -- cgit v1.2.3 From 6fb6b5f1766a1f37a61b9faaa40209bab995bf30 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Sun, 14 Apr 2024 08:18:36 -0600 Subject: Validate item access (#11171) --- .../Library/LibraryManager.cs | 39 +++++-- .../Controllers/DisplayPreferencesController.cs | 2 +- Jellyfin.Api/Controllers/FilterController.cs | 2 +- Jellyfin.Api/Controllers/ImageController.cs | 20 ++-- Jellyfin.Api/Controllers/InstantMixController.cs | 60 +++++++++-- Jellyfin.Api/Controllers/ItemLookupController.cs | 13 ++- Jellyfin.Api/Controllers/ItemRefreshController.cs | 5 +- Jellyfin.Api/Controllers/ItemUpdateController.cs | 12 ++- Jellyfin.Api/Controllers/ItemsController.cs | 12 ++- Jellyfin.Api/Controllers/LibraryController.cs | 65 ++++++------ .../Controllers/LibraryStructureController.cs | 12 ++- Jellyfin.Api/Controllers/LiveTvController.cs | 27 ++++- Jellyfin.Api/Controllers/LyricsController.cs | 58 ++++------- Jellyfin.Api/Controllers/MediaInfoController.cs | 42 ++++++-- Jellyfin.Api/Controllers/PlaylistsController.cs | 7 +- Jellyfin.Api/Controllers/PlaystateController.cs | 21 ++-- Jellyfin.Api/Controllers/RemoteImageController.cs | 9 +- Jellyfin.Api/Controllers/SearchController.cs | 2 +- Jellyfin.Api/Controllers/SubtitleController.cs | 50 ++++++--- Jellyfin.Api/Controllers/TrickplayController.cs | 4 +- Jellyfin.Api/Controllers/TvShowsController.cs | 15 +-- .../Controllers/UniversalAudioController.cs | 27 +++-- Jellyfin.Api/Controllers/UserLibraryController.cs | 116 +++++---------------- .../Controllers/VideoAttachmentsController.cs | 5 +- Jellyfin.Api/Controllers/VideosController.cs | 31 +++--- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 14 +-- Jellyfin.Api/Helpers/StreamingHelpers.cs | 5 +- MediaBrowser.Controller/Library/ILibraryManager.cs | 20 ++++ 28 files changed, 414 insertions(+), 281 deletions(-) (limited to 'Jellyfin.Api/Controllers/PlaylistsController.cs') diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index bb5cc746e..0a4432bec 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 1cad66326..6d94d96f3 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 d6e043e6a..4abca3271 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 6b38fa7d3..8e8accab3 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 3cf485299..dcbacf1d7 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 e3aee1bf7..d009f80a9 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 0a8522e1c..c1343b130 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 9800248c6..83f308bb1 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 26ae1a820..6ffe6e7da 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 360389d29..3b4e80ff3 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,20 +459,18 @@ 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); - + var user = userId.IsNullOrEmpty() + ? null + : _userManager.GetUserById(userId.Value); + var item = _libraryManager.GetItemById(itemId, user); if (item is null) { - return NotFound("Item not found"); + return NotFound(); } var baseItemDtos = new List(); - var user = userId.IsNullOrEmpty() - ? null - : _userManager.GetUserById(userId.Value); - 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 23c430f85..c1d01a5c2 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 7768b3c45..2b26c01f8 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 f2b312b47..8eb4cadf8 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