diff options
| author | Shadowghost <Ghost_of_Stone@web.de> | 2024-03-26 15:29:48 +0100 |
|---|---|---|
| committer | Shadowghost <Ghost_of_Stone@web.de> | 2024-03-26 15:49:18 +0100 |
| commit | 88b3490d1756236d0c2fc00243420d45d149a5d1 (patch) | |
| tree | 6d793ccd54c92b984d5be072a5c358b67f5a49ce | |
| parent | 2e9aa146a56472af4dc285a2d2c70f58b41035e1 (diff) | |
Add playlist ACL endpoints
| -rw-r--r-- | Emby.Server.Implementations/Playlists/PlaylistManager.cs | 88 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/PlaylistsController.cs | 122 | ||||
| -rw-r--r-- | Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs | 2 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Playlists/IPlaylistManager.cs | 35 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Playlists/Playlist.cs | 29 | ||||
| -rw-r--r-- | MediaBrowser.Model/Entities/IHasShares.cs | 6 |
6 files changed, 235 insertions, 47 deletions
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index aea8d65322..6724d54d1a 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -59,6 +59,11 @@ namespace Emby.Server.Implementations.Playlists _appConfig = appConfig; } + public Playlist GetPlaylist(Guid userId, Guid playlistId) + { + return GetPlaylists(userId).Where(p => p.Id.Equals(playlistId)).FirstOrDefault(); + } + public IEnumerable<Playlist> GetPlaylists(Guid userId) { var user = _userManager.GetUserById(userId); @@ -160,7 +165,7 @@ namespace Emby.Server.Implementations.Playlists } } - private string GetTargetPath(string path) + private static string GetTargetPath(string path) { while (Directory.Exists(path)) { @@ -231,13 +236,8 @@ namespace Emby.Server.Implementations.Playlists // Update the playlist in the repository playlist.LinkedChildren = newLinkedChildren; - await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); - // Update the playlist on disk - if (playlist.IsFile) - { - SavePlaylistFile(playlist); - } + await UpdatePlaylist(playlist).ConfigureAwait(false); // Refresh playlist metadata _providerManager.QueueRefresh( @@ -266,12 +266,7 @@ namespace Emby.Server.Implementations.Playlists .Select(i => i.Item1) .ToArray(); - await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); - - if (playlist.IsFile) - { - SavePlaylistFile(playlist); - } + await UpdatePlaylist(playlist).ConfigureAwait(false); _providerManager.QueueRefresh( playlist.Id, @@ -313,14 +308,9 @@ namespace Emby.Server.Implementations.Playlists newList.Insert(newIndex, item); } - playlist.LinkedChildren = newList.ToArray(); - - await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); + playlist.LinkedChildren = [.. newList]; - if (playlist.IsFile) - { - SavePlaylistFile(playlist); - } + await UpdatePlaylist(playlist).ConfigureAwait(false); } /// <inheritdoc /> @@ -430,8 +420,11 @@ namespace Emby.Server.Implementations.Playlists } else if (extension.Equals(".m3u8", StringComparison.OrdinalIgnoreCase)) { - var playlist = new M3uPlaylist(); - playlist.IsExtended = true; + var playlist = new M3uPlaylist + { + IsExtended = true + }; + foreach (var child in item.GetLinkedChildren()) { var entry = new M3uPlaylistEntry() @@ -481,7 +474,7 @@ namespace Emby.Server.Implementations.Playlists } } - private string NormalizeItemPath(string playlistPath, string itemPath) + private static string NormalizeItemPath(string playlistPath, string itemPath) { return MakeRelativePath(Path.GetDirectoryName(playlistPath), itemPath); } @@ -541,12 +534,7 @@ namespace Emby.Server.Implementations.Playlists { playlist.OwnerUserId = guid; playlist.Shares = rankedShares.Skip(1).ToArray(); - await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); - - if (playlist.IsFile) - { - SavePlaylistFile(playlist); - } + await UpdatePlaylist(playlist).ConfigureAwait(false); } else if (!playlist.OpenAccess) { @@ -563,5 +551,47 @@ namespace Emby.Server.Implementations.Playlists } } } + + public async Task ToggleOpenAccess(Guid playlistId, Guid userId) + { + var playlist = GetPlaylist(userId, playlistId); + playlist.OpenAccess = !playlist.OpenAccess; + + await UpdatePlaylist(playlist).ConfigureAwait(false); + } + + public async Task AddToShares(Guid playlistId, Guid userId, Share share) + { + var playlist = GetPlaylist(userId, playlistId); + var shares = playlist.Shares.ToList(); + var existingUserShare = shares.FirstOrDefault(s => s.UserId?.Equals(share.UserId, StringComparison.OrdinalIgnoreCase) ?? false); + if (existingUserShare is not null) + { + shares.Remove(existingUserShare); + } + + shares.Add(share); + playlist.Shares = shares; + await UpdatePlaylist(playlist).ConfigureAwait(false); + } + + public async Task RemoveFromShares(Guid playlistId, Guid userId, Share share) + { + var playlist = GetPlaylist(userId, playlistId); + var shares = playlist.Shares.ToList(); + shares.Remove(share); + playlist.Shares = shares; + await UpdatePlaylist(playlist).ConfigureAwait(false); + } + + private async Task UpdatePlaylist(Playlist playlist) + { + await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); + + if (playlist.IsFile) + { + SavePlaylistFile(playlist); + } + } } } diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 0e7c3f1556..f0e8227fda 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -99,6 +99,128 @@ public class PlaylistsController : BaseJellyfinApiController } /// <summary> + /// Get a playlist's shares. + /// </summary> + /// <param name="playlistId">The playlist id.</param> + /// <returns> + /// A list of <see cref="Share"/> objects. + /// </returns> + [HttpGet("{playlistId}/Shares")] + [ProducesResponseType(StatusCodes.Status200OK)] + public IReadOnlyList<Share> 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<Share>(); + } + + /// <summary> + /// Toggles OpenAccess of a playlist. + /// </summary> + /// <param name="playlistId">The playlist id.</param> + /// <returns> + /// A <see cref="Task" /> that represents the asynchronous operation to toggle OpenAccess of a playlist. + /// The task result contains an <see cref="OkResult"/> indicating success. + /// </returns> + [HttpPost("{playlistId}/ToggleOpenAccess")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task<ActionResult> 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(); + } + + /// <summary> + /// Adds shares to a playlist's shares. + /// </summary> + /// <param name="playlistId">The playlist id.</param> + /// <param name="shares">The shares.</param> + /// <returns> + /// A <see cref="Task" /> that represents the asynchronous operation to add shares to a playlist. + /// The task result contains an <see cref="OkResult"/> indicating success. + /// </returns> + [HttpPost("{playlistId}/Shares")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task<ActionResult> 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(); + } + + /// <summary> + /// Remove a user from a playlist's shares. + /// </summary> + /// <param name="playlistId">The playlist id.</param> + /// <param name="userId">The user id.</param> + /// <returns> + /// A <see cref="Task" /> that represents the asynchronous operation to delete a user from a playlist's shares. + /// The task result contains an <see cref="OkResult"/> indicating success. + /// </returns> + [HttpDelete("{playlistId}/Shares")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task<ActionResult> 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(); + } + + /// <summary> /// Adds items to a playlist. /// </summary> /// <param name="playlistId">The playlist id.</param> diff --git a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs index cf31820034..06596c171f 100644 --- a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs +++ b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs @@ -54,7 +54,7 @@ internal class FixPlaylistOwner : IMigrationRoutine foreach (var playlist in playlists) { var shares = playlist.Shares; - if (shares.Length > 0) + if (shares.Count > 0) { var firstEditShare = shares.First(x => x.CanEdit); if (firstEditShare is not null && Guid.TryParse(firstEditShare.UserId, out var guid)) diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index bb68a3b6dd..aaca1cc492 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.Playlists; namespace MediaBrowser.Controller.Playlists @@ -11,6 +12,14 @@ namespace MediaBrowser.Controller.Playlists public interface IPlaylistManager { /// <summary> + /// Gets the playlist. + /// </summary> + /// <param name="userId">The user identifier.</param> + /// <param name="playlistId">The playlist identifier.</param> + /// <returns>Playlist.</returns> + Playlist GetPlaylist(Guid userId, Guid playlistId); + + /// <summary> /// Gets the playlists. /// </summary> /// <param name="userId">The user identifier.</param> @@ -18,6 +27,32 @@ namespace MediaBrowser.Controller.Playlists IEnumerable<Playlist> GetPlaylists(Guid userId); /// <summary> + /// Toggle OpenAccess policy of the playlist. + /// </summary> + /// <param name="playlistId">The playlist identifier.</param> + /// <param name="userId">The user identifier.</param> + /// <returns>Task.</returns> + Task ToggleOpenAccess(Guid playlistId, Guid userId); + + /// <summary> + /// Adds a share to the playlist. + /// </summary> + /// <param name="playlistId">The playlist identifier.</param> + /// <param name="userId">The user identifier.</param> + /// <param name="share">The share.</param> + /// <returns>Task.</returns> + Task AddToShares(Guid playlistId, Guid userId, Share share); + + /// <summary> + /// Rremoves a share from the playlist. + /// </summary> + /// <param name="playlistId">The playlist identifier.</param> + /// <param name="userId">The user identifier.</param> + /// <param name="share">The share.</param> + /// <returns>Task.</returns> + Task RemoveFromShares(Guid playlistId, Guid userId, Share share); + + /// <summary> /// Creates the playlist. /// </summary> /// <param name="options">The options.</param> diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index ca032e7f6e..9a08a4ce3e 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -16,20 +16,19 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Querying; namespace MediaBrowser.Controller.Playlists { public class Playlist : Folder, IHasShares { - public static readonly IReadOnlyList<string> SupportedExtensions = new[] - { + public static readonly IReadOnlyList<string> SupportedExtensions = + [ ".m3u", ".m3u8", ".pls", ".wpl", ".zpl" - }; + ]; public Playlist() { @@ -41,7 +40,7 @@ namespace MediaBrowser.Controller.Playlists public bool OpenAccess { get; set; } - public Share[] Shares { get; set; } + public IReadOnlyList<Share> Shares { get; set; } [JsonIgnore] public bool IsFile => IsPlaylistFile(Path); @@ -192,9 +191,9 @@ namespace MediaBrowser.Controller.Playlists return LibraryManager.GetItemList(new InternalItemsQuery(user) { Recursive = true, - IncludeItemTypes = new[] { BaseItemKind.Audio }, - GenreIds = new[] { musicGenre.Id }, - OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) }, + IncludeItemTypes = [BaseItemKind.Audio], + GenreIds = [musicGenre.Id], + OrderBy = [(ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending)], DtoOptions = options }); } @@ -204,9 +203,9 @@ namespace MediaBrowser.Controller.Playlists return LibraryManager.GetItemList(new InternalItemsQuery(user) { Recursive = true, - IncludeItemTypes = new[] { BaseItemKind.Audio }, - ArtistIds = new[] { musicArtist.Id }, - OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) }, + IncludeItemTypes = [BaseItemKind.Audio], + ArtistIds = [musicArtist.Id], + OrderBy = [(ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending)], DtoOptions = options }); } @@ -217,8 +216,8 @@ namespace MediaBrowser.Controller.Playlists { Recursive = true, IsFolder = false, - OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, - MediaTypes = new[] { mediaType }, + OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)], + MediaTypes = [mediaType], EnableTotalRecordCount = false, DtoOptions = options }; @@ -226,7 +225,7 @@ namespace MediaBrowser.Controller.Playlists return folder.GetItemList(query); } - return new[] { item }; + return [item]; } public override bool IsVisible(User user) @@ -248,7 +247,7 @@ namespace MediaBrowser.Controller.Playlists } var shares = Shares; - if (shares.Length == 0) + if (shares.Count == 0) { return false; } diff --git a/MediaBrowser.Model/Entities/IHasShares.cs b/MediaBrowser.Model/Entities/IHasShares.cs index b34d1a0376..31574a3ffa 100644 --- a/MediaBrowser.Model/Entities/IHasShares.cs +++ b/MediaBrowser.Model/Entities/IHasShares.cs @@ -1,4 +1,6 @@ -namespace MediaBrowser.Model.Entities; +using System.Collections.Generic; + +namespace MediaBrowser.Model.Entities; /// <summary> /// Interface for access to shares. @@ -8,5 +10,5 @@ public interface IHasShares /// <summary> /// Gets or sets the shares. /// </summary> - Share[] Shares { get; set; } + IReadOnlyList<Share> Shares { get; set; } } |
