Jelajahi Sumber

Add playlist ACL endpoints

Shadowghost 1 tahun lalu
induk
melakukan
88b3490d17

+ 59 - 29
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);
+            }
+        }
     }
 }

+ 122 - 0
Jellyfin.Api/Controllers/PlaylistsController.cs

@@ -98,6 +98,128 @@ public class PlaylistsController : BaseJellyfinApiController
         return result;
     }
 
+    /// <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>

+ 1 - 1
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))

+ 35 - 0
MediaBrowser.Controller/Playlists/IPlaylistManager.cs

@@ -4,12 +4,21 @@ using System;
 using System.Collections.Generic;
 using System.Threading.Tasks;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Playlists;
 
 namespace MediaBrowser.Controller.Playlists
 {
     public interface IPlaylistManager
     {
+        /// <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>
@@ -17,6 +26,32 @@ namespace MediaBrowser.Controller.Playlists
         /// <returns>IEnumerable&lt;Playlist&gt;.</returns>
         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>

+ 14 - 15
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;
             }

+ 4 - 2
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; }
 }