aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2024-03-26 15:29:48 +0100
committerShadowghost <Ghost_of_Stone@web.de>2024-03-26 15:49:18 +0100
commit88b3490d1756236d0c2fc00243420d45d149a5d1 (patch)
tree6d793ccd54c92b984d5be072a5c358b67f5a49ce
parent2e9aa146a56472af4dc285a2d2c70f58b41035e1 (diff)
Add playlist ACL endpoints
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs88
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs122
-rw-r--r--Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs2
-rw-r--r--MediaBrowser.Controller/Playlists/IPlaylistManager.cs35
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs29
-rw-r--r--MediaBrowser.Model/Entities/IHasShares.cs6
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; }
}