Browse Source

Merge pull request #9466 from Shadowghost/playlist-fix

Bond-009 2 years ago
parent
commit
9c500bdca3

+ 52 - 10
Emby.Server.Implementations/Playlists/PlaylistManager.cs

@@ -135,16 +135,8 @@ namespace Emby.Server.Implementations.Playlists
                 {
                     Name = name,
                     Path = path,
-                    Shares = new[]
-                    {
-                        new Share
-                        {
-                            UserId = options.UserId.Equals(default)
-                                ? null
-                                : options.UserId.ToString("N", CultureInfo.InvariantCulture),
-                            CanEdit = true
-                        }
-                    }
+                    OwnerUserId = options.UserId,
+                    Shares = options.Shares ?? Array.Empty<Share>()
                 };
 
                 playlist.SetMediaType(options.MediaType);
@@ -537,5 +529,55 @@ namespace Emby.Server.Implementations.Playlists
             return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal)) ??
                 _libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal));
         }
+
+        /// <inheritdoc />
+        public async Task RemovePlaylistsAsync(Guid userId)
+        {
+            var playlists = GetPlaylists(userId);
+            foreach (var playlist in 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))
+                {
+                    playlist.OwnerUserId = guid;
+                    playlist.Shares = rankedShares.Skip(1).ToArray();
+                    await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+
+                    if (playlist.IsFile)
+                    {
+                        SavePlaylistFile(playlist);
+                    }
+                }
+                else
+                {
+                    // Remove playlist if not shared
+                    _libraryManager.DeleteItem(
+                        playlist,
+                        new DeleteOptions
+                        {
+                            DeleteFileLocation = false,
+                            DeleteFromExternalProvider = false
+                        },
+                        playlist.GetParent(),
+                        false);
+                }
+            }
+        }
+
+        /// <inheritdoc />
+        public async Task UpdatePlaylistAsync(Playlist playlist)
+        {
+            var currentPlaylist = (Playlist)_libraryManager.GetItemById(playlist.Id);
+            currentPlaylist.OwnerUserId = playlist.OwnerUserId;
+            currentPlaylist.Shares = playlist.Shares;
+
+            await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+
+            if (currentPlaylist.IsFile)
+            {
+                SavePlaylistFile(currentPlaylist);
+            }
+        }
     }
 }

+ 7 - 1
Jellyfin.Api/Controllers/UserController.cs

@@ -15,6 +15,7 @@ using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.QuickConnect;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Configuration;
@@ -41,6 +42,7 @@ public class UserController : BaseJellyfinApiController
     private readonly IServerConfigurationManager _config;
     private readonly ILogger _logger;
     private readonly IQuickConnect _quickConnectManager;
+    private readonly IPlaylistManager _playlistManager;
 
     /// <summary>
     /// Initializes a new instance of the <see cref="UserController"/> class.
@@ -53,6 +55,7 @@ public class UserController : BaseJellyfinApiController
     /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
     /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
     /// <param name="quickConnectManager">Instance of the <see cref="IQuickConnect"/> interface.</param>
+    /// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param>
     public UserController(
         IUserManager userManager,
         ISessionManager sessionManager,
@@ -61,7 +64,8 @@ public class UserController : BaseJellyfinApiController
         IAuthorizationContext authContext,
         IServerConfigurationManager config,
         ILogger<UserController> logger,
-        IQuickConnect quickConnectManager)
+        IQuickConnect quickConnectManager,
+        IPlaylistManager playlistManager)
     {
         _userManager = userManager;
         _sessionManager = sessionManager;
@@ -71,6 +75,7 @@ public class UserController : BaseJellyfinApiController
         _config = config;
         _logger = logger;
         _quickConnectManager = quickConnectManager;
+        _playlistManager = playlistManager;
     }
 
     /// <summary>
@@ -153,6 +158,7 @@ public class UserController : BaseJellyfinApiController
         }
 
         await _sessionManager.RevokeUserTokens(user.Id, null).ConfigureAwait(false);
+        await _playlistManager.RemovePlaylistsAsync(userId).ConfigureAwait(false);
         await _userManager.DeleteUserAsync(userId).ConfigureAwait(false);
         return NoContent();
     }

+ 2 - 1
Jellyfin.Server/Migrations/MigrationRunner.cs

@@ -40,7 +40,8 @@ namespace Jellyfin.Server.Migrations
             typeof(Routines.ReaddDefaultPluginRepository),
             typeof(Routines.MigrateDisplayPreferencesDb),
             typeof(Routines.RemoveDownloadImagesInAdvance),
-            typeof(Routines.MigrateAuthenticationDb)
+            typeof(Routines.MigrateAuthenticationDb),
+            typeof(Routines.FixPlaylistOwner)
         };
 
         /// <summary>

+ 67 - 0
Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs

@@ -0,0 +1,67 @@
+using System;
+using System.Linq;
+
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Playlists;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Properly set playlist owner.
+/// </summary>
+internal class FixPlaylistOwner : IMigrationRoutine
+{
+    private readonly ILogger<RemoveDuplicateExtras> _logger;
+    private readonly ILibraryManager _libraryManager;
+    private readonly IPlaylistManager _playlistManager;
+
+    public FixPlaylistOwner(
+        ILogger<RemoveDuplicateExtras> logger,
+        ILibraryManager libraryManager,
+        IPlaylistManager playlistManager)
+    {
+        _logger = logger;
+        _libraryManager = libraryManager;
+        _playlistManager = playlistManager;
+    }
+
+    /// <inheritdoc/>
+    public Guid Id => Guid.Parse("{615DFA9E-2497-4DBB-A472-61938B752C5B}");
+
+    /// <inheritdoc/>
+    public string Name => "FixPlaylistOwner";
+
+    /// <inheritdoc/>
+    public bool PerformOnNewInstall => false;
+
+    /// <inheritdoc/>
+    public void Perform()
+    {
+        var playlists = _libraryManager.GetItemList(new InternalItemsQuery
+        {
+            IncludeItemTypes = new[] { BaseItemKind.Playlist }
+        })
+        .Cast<Playlist>()
+        .Where(x => x.OwnerUserId.Equals(Guid.Empty))
+        .ToArray();
+
+        if (playlists.Length > 0)
+        {
+            foreach (var playlist in playlists)
+            {
+                var shares = playlist.Shares;
+                var firstEditShare = shares.First(x => x.CanEdit);
+                if (firstEditShare is not null && Guid.TryParse(firstEditShare.UserId, out var guid))
+                {
+                    playlist.OwnerUserId = guid;
+                    playlist.Shares = shares.Where(x => x != firstEditShare).ToArray();
+
+                    _playlistManager.UpdatePlaylistAsync(playlist).GetAwaiter().GetResult();
+                }
+            }
+        }
+    }
+}

+ 0 - 11
MediaBrowser.Controller/Entities/IHasShares.cs

@@ -1,11 +0,0 @@
-#nullable disable
-
-#pragma warning disable CA1819, CS1591
-
-namespace MediaBrowser.Controller.Entities
-{
-    public interface IHasShares
-    {
-        Share[] Shares { get; set; }
-    }
-}

+ 0 - 13
MediaBrowser.Controller/Entities/Share.cs

@@ -1,13 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Controller.Entities
-{
-    public class Share
-    {
-        public string UserId { get; set; }
-
-        public bool CanEdit { get; set; }
-    }
-}

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

@@ -56,5 +56,20 @@ namespace MediaBrowser.Controller.Playlists
         /// <param name="newIndex">The new index.</param>
         /// <returns>Task.</returns>
         Task MoveItemAsync(string playlistId, string entryId, int newIndex);
+
+        /// <summary>
+        /// Removed all playlists of a user.
+        /// If the playlist is shared, ownership is transferred.
+        /// </summary>
+        /// <param name="userId">The user id.</param>
+        /// <returns>Task.</returns>
+        Task RemovePlaylistsAsync(Guid userId);
+
+        /// <summary>
+        /// Updates a playlist.
+        /// </summary>
+        /// <param name="playlist">The updated playlist.</param>
+        /// <returns>Task.</returns>
+        Task UpdatePlaylistAsync(Playlist playlist);
     }
 }

+ 4 - 3
MediaBrowser.Controller/Playlists/Playlist.cs

@@ -15,6 +15,7 @@ using MediaBrowser.Controller.Dto;
 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
@@ -232,7 +233,8 @@ namespace MediaBrowser.Controller.Playlists
                 return base.IsVisible(user);
             }
 
-            if (user.Id.Equals(OwnerUserId))
+            var userId = user.Id;
+            if (userId.Equals(OwnerUserId))
             {
                 return true;
             }
@@ -240,10 +242,9 @@ namespace MediaBrowser.Controller.Playlists
             var shares = Shares;
             if (shares.Length == 0)
             {
-                return base.IsVisible(user);
+                return false;
             }
 
-            var userId = user.Id;
             return shares.Any(share => Guid.TryParse(share.UserId, out var id) && id.Equals(userId));
         }
 

+ 16 - 0
MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs

@@ -9,6 +9,7 @@ using System.Xml;
 using Jellyfin.Data.Enums;
 using Jellyfin.Extensions;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using Microsoft.Extensions.Logging;
@@ -637,6 +638,21 @@ namespace MediaBrowser.LocalMetadata.Parsers
                     break;
                 }
 
+                case "OwnerUserId":
+                {
+                    var val = reader.ReadElementContentAsString();
+
+                    if (Guid.TryParse(val, out var guid) && !guid.Equals(Guid.Empty))
+                    {
+                        if (item is Playlist playlist)
+                        {
+                            playlist.OwnerUserId = guid;
+                        }
+                    }
+
+                    break;
+                }
+
                 case "Format3D":
                 {
                     var val = reader.ReadElementContentAsString();

+ 12 - 8
MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs

@@ -395,6 +395,7 @@ namespace MediaBrowser.LocalMetadata.Savers
 
             if (item is Playlist playlist && !Playlist.IsPlaylistFile(playlist.Path))
             {
+                await writer.WriteElementStringAsync(null, "OwnerUserId", null, playlist.OwnerUserId.ToString("N")).ConfigureAwait(false);
                 await AddLinkedChildren(playlist, writer, "PlaylistItems", "PlaylistItem").ConfigureAwait(false);
             }
 
@@ -418,16 +419,19 @@ namespace MediaBrowser.LocalMetadata.Savers
 
             foreach (var share in item.Shares)
             {
-                await writer.WriteStartElementAsync(null, "Share", null).ConfigureAwait(false);
+                if (share.UserId is not null)
+                {
+                    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).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);

+ 12 - 0
MediaBrowser.Model/Entities/IHasShares.cs

@@ -0,0 +1,12 @@
+namespace MediaBrowser.Model.Entities;
+
+/// <summary>
+/// Interface for access to shares.
+/// </summary>
+public interface IHasShares
+{
+    /// <summary>
+    /// Gets or sets the shares.
+    /// </summary>
+    Share[] Shares { get; set; }
+}

+ 17 - 0
MediaBrowser.Model/Entities/Share.cs

@@ -0,0 +1,17 @@
+namespace MediaBrowser.Model.Entities;
+
+/// <summary>
+/// Class to hold data on sharing permissions.
+/// </summary>
+public class Share
+{
+    /// <summary>
+    /// Gets or sets the user id.
+    /// </summary>
+    public string? UserId { get; set; }
+
+    /// <summary>
+    /// Gets or sets a value indicating whether the user has edit permissions.
+    /// </summary>
+    public bool CanEdit { get; set; }
+}

+ 28 - 11
MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs

@@ -1,19 +1,36 @@
-#nullable disable
-#pragma warning disable CS1591
-
 using System;
 using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Model.Playlists;
 
-namespace MediaBrowser.Model.Playlists
+/// <summary>
+/// A playlist creation request.
+/// </summary>
+public class PlaylistCreationRequest
 {
-    public class PlaylistCreationRequest
-    {
-        public string Name { get; set; }
+    /// <summary>
+    /// Gets or sets the name.
+    /// </summary>
+    public string? Name { get; set; }
+
+    /// <summary>
+    /// Gets or sets the list of items.
+    /// </summary>
+    public IReadOnlyList<Guid> ItemIdList { get; set; } = Array.Empty<Guid>();
 
-        public IReadOnlyList<Guid> ItemIdList { get; set; } = Array.Empty<Guid>();
+    /// <summary>
+    /// Gets or sets the media type.
+    /// </summary>
+    public string? MediaType { get; set; }
 
-        public string MediaType { get; set; }
+    /// <summary>
+    /// Gets or sets the user id.
+    /// </summary>
+    public Guid UserId { get; set; }
 
-        public Guid UserId { get; set; }
-    }
+    /// <summary>
+    /// Gets or sets the shares.
+    /// </summary>
+    public Share[]? Shares { get; set; }
 }