aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoshua M. Boniface <joshua@boniface.me>2024-04-10 12:52:01 -0400
committerGitHub <noreply@github.com>2024-04-10 12:52:01 -0400
commitee1d6332ee720ceeccc7b051465ce61a6628b3dc (patch)
treec5fe5b5f3075bc69b1f0dcd499c3e95e365a3fbd
parentdc74bc361d586130ed04b24d85456013975d9f44 (diff)
parentddda30fe23a04e07401bc870ac33213ff8a34c71 (diff)
Merge pull request #11220 from Shadowghost/add-playlist-acl-api
Add playlist ACL endpoints
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs9
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs174
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs303
-rw-r--r--Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs15
-rw-r--r--Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs34
-rw-r--r--Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistUserDto.cs12
-rw-r--r--Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs6
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs2
-rw-r--r--MediaBrowser.Controller/Playlists/IPlaylistManager.cs44
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs44
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs18
-rw-r--r--MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs19
-rw-r--r--MediaBrowser.Model/Entities/IHasShares.cs6
-rw-r--r--MediaBrowser.Model/Entities/PlaylistUserPermissions.cs30
-rw-r--r--MediaBrowser.Model/Entities/Share.cs17
-rw-r--r--MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs11
-rw-r--r--MediaBrowser.Model/Playlists/PlaylistUpdateRequest.cs41
-rw-r--r--MediaBrowser.Model/Playlists/PlaylistUserUpdateRequest.cs24
18 files changed, 663 insertions, 146 deletions
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
/// </summary>
public class PlaylistResolver : GenericFolderResolver<Playlist>
{
- private CollectionType?[] _musicPlaylistCollectionTypes =
- {
+ private readonly CollectionType?[] _musicPlaylistCollectionTypes =
+ [
null,
CollectionType.music
- };
+ ];
/// <inheritdoc/>
protected override Playlist Resolve(ItemResolveArgs args)
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index aea8d6532..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,6 +60,11 @@ namespace Emby.Server.Implementations.Playlists
_appConfig = appConfig;
}
+ public Playlist GetPlaylistForUser(Guid playlistId, Guid userId)
+ {
+ return GetPlaylists(userId).Where(p => p.Id.Equals(playlistId)).FirstOrDefault();
+ }
+
public IEnumerable<Playlist> GetPlaylists(Guid userId)
{
var user = _userManager.GetUserById(userId);
@@ -66,61 +72,56 @@ namespace Emby.Server.Implementations.Playlists
return GetPlaylistsFolder(userId).GetChildren(user, true).OfType<Playlist>();
}
- public async Task<PlaylistCreationResult> CreatePlaylist(PlaylistCreationRequest options)
+ public async Task<PlaylistCreationResult> 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);
- 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;
+ 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,19 +134,20 @@ namespace Emby.Server.Implementations.Playlists
{
Name = name,
Path = path,
- OwnerUserId = options.UserId,
- Shares = options.Shares ?? Array.Empty<Share>()
+ 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);
@@ -160,7 +162,7 @@ namespace Emby.Server.Implementations.Playlists
}
}
- private string GetTargetPath(string path)
+ private static string GetTargetPath(string path)
{
while (Directory.Exists(path))
{
@@ -170,14 +172,14 @@ namespace Emby.Server.Implementations.Playlists
return path;
}
- private List<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, MediaType playlistMediaType, User user, DtoOptions options)
+ private IReadOnlyList<BaseItem> GetPlaylistItems(IEnumerable<Guid> 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);
}
- public Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
+ public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
{
var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
@@ -231,13 +233,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 UpdatePlaylistInternal(playlist).ConfigureAwait(false);
// Refresh playlist metadata
_providerManager.QueueRefresh(
@@ -249,7 +246,7 @@ namespace Emby.Server.Implementations.Playlists
RefreshPriority.High);
}
- public async Task RemoveFromPlaylistAsync(string playlistId, IEnumerable<string> entryIds)
+ public async Task RemoveItemFromPlaylistAsync(string playlistId, IEnumerable<string> entryIds)
{
if (_libraryManager.GetItemById(playlistId) is not Playlist playlist)
{
@@ -266,12 +263,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 UpdatePlaylistInternal(playlist).ConfigureAwait(false);
_providerManager.QueueRefresh(
playlist.Id,
@@ -313,14 +305,9 @@ namespace Emby.Server.Implementations.Playlists
newList.Insert(newIndex, item);
}
- playlist.LinkedChildren = newList.ToArray();
+ playlist.LinkedChildren = [.. newList];
- await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
-
- if (playlist.IsFile)
- {
- SavePlaylistFile(playlist);
- }
+ await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -430,8 +417,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 +471,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);
}
@@ -537,16 +527,11 @@ 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 playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
-
- if (playlist.IsFile)
- {
- SavePlaylistFile(playlist);
- }
+ await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
}
else if (!playlist.OpenAccess)
{
@@ -563,5 +548,76 @@ namespace Emby.Server.Implementations.Playlists
}
}
}
+
+ public async Task UpdatePlaylist(PlaylistUpdateRequest request)
+ {
+ var playlist = GetPlaylistForUser(request.Id, request.UserId);
+
+ 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 = GetPlaylistForUser(request.Id, request.UserId);
+ }
+
+ 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 UpdatePlaylistInternal(playlist).ConfigureAwait(false);
+ }
+
+ public async Task AddUserToShares(PlaylistUserUpdateRequest request)
+ {
+ var userId = request.UserId;
+ var playlist = GetPlaylistForUser(request.Id, userId);
+ var shares = playlist.Shares.ToList();
+ var existingUserShare = shares.FirstOrDefault(s => s.UserId.Equals(userId));
+ if (existingUserShare is not null)
+ {
+ shares.Remove(existingUserShare);
+ }
+
+ shares.Add(new PlaylistUserPermissions(userId, request.CanEdit ?? false));
+ playlist.Shares = shares;
+ await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
+ }
+
+ public async Task RemoveUserFromShares(Guid playlistId, Guid userId, PlaylistUserPermissions share)
+ {
+ var playlist = GetPlaylistForUser(playlistId, userId);
+ var shares = playlist.Shares.ToList();
+ shares.Remove(share);
+ playlist.Shares = shares;
+ await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
+ }
+
+ private async Task UpdatePlaylistInternal(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 0e7c3f155..1100f85cf 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -92,29 +92,267 @@ public class PlaylistsController : BaseJellyfinApiController
Name = name ?? createPlaylistRequest?.Name,
ItemIdList = ids,
UserId = userId.Value,
- MediaType = mediaType ?? createPlaylistRequest?.MediaType
+ MediaType = mediaType ?? createPlaylistRequest?.MediaType,
+ Users = createPlaylistRequest?.Users.ToArray() ?? [],
+ Public = createPlaylistRequest?.IsPublic
}).ConfigureAwait(false);
return result;
}
/// <summary>
+ /// Updates a playlist.
+ /// </summary>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <param name="updatePlaylistRequest">The <see cref="UpdatePlaylistDto"/> id.</param>
+ /// <response code="204">Playlist updated.</response>
+ /// <response code="403">Access forbidden.</response>
+ /// <response code="404">Playlist not found.</response>
+ /// <returns>
+ /// A <see cref="Task" /> that represents the asynchronous operation to update a playlist.
+ /// The task result contains an <see cref="OkResult"/> indicating success.
+ /// </returns>
+ [HttpPost("{playlistId}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> UpdatePlaylist(
+ [FromRoute, Required] Guid playlistId,
+ [FromBody, Required] UpdatePlaylistDto updatePlaylistRequest)
+ {
+ var callingUserId = User.GetUserId();
+
+ var playlist = _playlistManager.GetPlaylistForUser(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.UpdatePlaylist(new PlaylistUpdateRequest
+ {
+ UserId = callingUserId,
+ Id = playlistId,
+ Name = updatePlaylistRequest.Name,
+ Ids = updatePlaylistRequest.Ids,
+ Users = updatePlaylistRequest.Users,
+ Public = updatePlaylistRequest.IsPublic
+ }).ConfigureAwait(false);
+
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Get a playlist's users.
+ /// </summary>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <response code="200">Found shares.</response>
+ /// <response code="403">Access forbidden.</response>
+ /// <response code="404">Playlist not found.</response>
+ /// <returns>
+ /// A list of <see cref="PlaylistUserPermissions"/> objects.
+ /// </returns>
+ [HttpGet("{playlistId}/Users")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<IReadOnlyList<PlaylistUserPermissions>> GetPlaylistUsers(
+ [FromRoute, Required] Guid playlistId)
+ {
+ var userId = User.GetUserId();
+
+ var playlist = _playlistManager.GetPlaylistForUser(playlistId, userId);
+ if (playlist is null)
+ {
+ return NotFound("Playlist not found");
+ }
+
+ var isPermitted = playlist.OwnerUserId.Equals(userId);
+
+ return isPermitted ? playlist.Shares.ToList() : Forbid();
+ }
+
+ /// <summary>
+ /// Get a playlist user.
+ /// </summary>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <param name="userId">The user id.</param>
+ /// <response code="200">User permission found.</response>
+ /// <response code="403">Access forbidden.</response>
+ /// <response code="404">Playlist not found.</response>
+ /// <returns>
+ /// <see cref="PlaylistUserPermissions"/>.
+ /// </returns>
+ [HttpGet("{playlistId}/Users/{userId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<PlaylistUserPermissions?> 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 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);
+
+ if (!isPermitted)
+ {
+ return Forbid();
+ }
+
+ if (userPermission is not null)
+ {
+ return userPermission;
+ }
+
+ return NotFound("User permissions not found");
+ }
+
+ /// <summary>
+ /// Modify a user of a playlist's users.
+ /// </summary>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <param name="userId">The user id.</param>
+ /// <param name="updatePlaylistUserRequest">The <see cref="UpdatePlaylistUserDto"/>.</param>
+ /// <response code="204">User's permissions modified.</response>
+ /// <response code="403">Access forbidden.</response>
+ /// <response code="404">Playlist not found.</response>
+ /// <returns>
+ /// A <see cref="Task" /> that represents the asynchronous operation to modify an user's playlist permissions.
+ /// The task result contains an <see cref="OkResult"/> indicating success.
+ /// </returns>
+ [HttpPost("{playlistId}/Users/{userId}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> UpdatePlaylistUser(
+ [FromRoute, Required] Guid playlistId,
+ [FromRoute, Required] Guid userId,
+ [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow), Required] UpdatePlaylistUserDto updatePlaylistUserRequest)
+ {
+ var callingUserId = User.GetUserId();
+
+ var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId);
+ if (playlist is null)
+ {
+ return NotFound("Playlist not found");
+ }
+
+ var isPermitted = playlist.OwnerUserId.Equals(callingUserId);
+
+ if (!isPermitted)
+ {
+ return Forbid();
+ }
+
+ await _playlistManager.AddUserToShares(new PlaylistUserUpdateRequest
+ {
+ Id = playlistId,
+ UserId = userId,
+ CanEdit = updatePlaylistUserRequest.CanEdit
+ }).ConfigureAwait(false);
+
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Remove a user from a playlist's users.
+ /// </summary>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <param name="userId">The user id.</param>
+ /// <response code="204">User permissions removed from playlist.</response>
+ /// <response code="401">Unauthorized access.</response>
+ /// <response code="404">No playlist or user permissions found.</response>
+ /// <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}/Users/{userId}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> RemoveUserFromPlaylist(
+ [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(callingUserId)
+ || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId));
+
+ if (!isPermitted)
+ {
+ return Forbid();
+ }
+
+ var share = playlist.Shares.FirstOrDefault(s => s.UserId.Equals(userId));
+ if (share is null)
+ {
+ return NotFound("User permissions not found");
+ }
+
+ await _playlistManager.RemoveUserFromShares(playlistId, callingUserId, share).ConfigureAwait(false);
+
+ return NoContent();
+ }
+
+ /// <summary>
/// Adds items to a playlist.
/// </summary>
/// <param name="playlistId">The playlist id.</param>
/// <param name="ids">Item id, comma delimited.</param>
/// <param name="userId">The userId.</param>
/// <response code="204">Items added to playlist.</response>
+ /// <response code="403">Access forbidden.</response>
+ /// <response code="404">Playlist not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
[HttpPost("{playlistId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> AddToPlaylist(
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> 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);
+ 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();
}
@@ -125,14 +363,34 @@ public class PlaylistsController : BaseJellyfinApiController
/// <param name="itemId">The item id.</param>
/// <param name="newIndex">The new index.</param>
/// <response code="204">Item moved to new index.</response>
+ /// <response code="403">Access forbidden.</response>
+ /// <response code="404">Playlist not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
[HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> 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();
}
@@ -143,14 +401,34 @@ public class PlaylistsController : BaseJellyfinApiController
/// <param name="playlistId">The playlist id.</param>
/// <param name="entryIds">The item ids, comma delimited.</param>
/// <response code="204">Items removed.</response>
+ /// <response code="403">Access forbidden.</response>
+ /// <response code="404">Playlist not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
[HttpDelete("{playlistId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> RemoveFromPlaylist(
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> RemoveItemFromPlaylist(
[FromRoute, Required] string playlistId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds)
{
- await _playlistManager.RemoveFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false);
+ 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();
}
@@ -167,10 +445,12 @@ public class PlaylistsController : BaseJellyfinApiController
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Original playlist returned.</response>
+ /// <response code="404">Access forbidden.</response>
/// <response code="404">Playlist not found.</response>
/// <returns>The original playlist items.</returns>
[HttpGet("{playlistId}/Items")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetPlaylistItems(
[FromRoute, Required] Guid playlistId,
@@ -184,10 +464,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/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
index bdc488871..3cbdd031a 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;
@@ -14,13 +15,13 @@ public class CreatePlaylistDto
/// <summary>
/// Gets or sets the name of the new playlist.
/// </summary>
- public string? Name { get; set; }
+ public required string Name { get; set; }
/// <summary>
/// Gets or sets item ids to add to the playlist.
/// </summary>
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList<Guid> Ids { get; set; } = Array.Empty<Guid>();
+ public IReadOnlyList<Guid> Ids { get; set; } = [];
/// <summary>
/// Gets or sets the user id.
@@ -31,4 +32,14 @@ public class CreatePlaylistDto
/// Gets or sets the media type.
/// </summary>
public MediaType? MediaType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the playlist users.
+ /// </summary>
+ public IReadOnlyList<PlaylistUserPermissions> Users { get; set; } = [];
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the playlist is public.
+ /// </summary>
+ public bool IsPublic { get; set; } = true;
}
diff --git a/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs
new file mode 100644
index 000000000..80e20995c
--- /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;
+
+/// <summary>
+/// Update existing playlist dto. Fields set to `null` will not be updated and keep their current values.
+/// </summary>
+public class UpdatePlaylistDto
+{
+ /// <summary>
+ /// Gets or sets the name of the new playlist.
+ /// </summary>
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets item ids of the playlist.
+ /// </summary>
+ [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ public IReadOnlyList<Guid>? Ids { get; set; }
+
+ /// <summary>
+ /// Gets or sets the playlist users.
+ /// </summary>
+ public IReadOnlyList<PlaylistUserPermissions>? Users { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the playlist is public.
+ /// </summary>
+ public bool? IsPublic { get; set; }
+}
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;
+
+/// <summary>
+/// Update existing playlist user dto. Fields set to `null` will not be updated and keep their current values.
+/// </summary>
+public class UpdatePlaylistUserDto
+{
+ /// <summary>
+ /// Gets or sets a value indicating whether the user can edit the playlist.
+ /// </summary>
+ public bool? CanEdit { get; set; }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
index cf3182003..3655a610d 100644
--- a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
+++ b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
@@ -54,12 +54,12 @@ 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))
+ 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/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index ac9698ec9..5f9840b1b 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -833,7 +833,7 @@ namespace MediaBrowser.Controller.Entities
return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders);
}
- public bool CanDelete(User user)
+ public virtual bool CanDelete(User user)
{
var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
index bb68a3b6d..cbe4bd87f 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,28 @@ namespace MediaBrowser.Controller.Playlists
public interface IPlaylistManager
{
/// <summary>
+ /// Gets the playlist.
+ /// </summary>
+ /// <param name="playlistId">The playlist identifier.</param>
+ /// <param name="userId">The user identifier.</param>
+ /// <returns>Playlist.</returns>
+ Playlist GetPlaylistForUser(Guid playlistId, Guid userId);
+
+ /// <summary>
+ /// Creates the playlist.
+ /// </summary>
+ /// <param name="request">The <see cref="PlaylistCreationRequest"/>.</param>
+ /// <returns>The created playlist.</returns>
+ Task<PlaylistCreationResult> CreatePlaylist(PlaylistCreationRequest request);
+
+ /// <summary>
+ /// Updates a playlist.
+ /// </summary>
+ /// <param name="request">The <see cref="PlaylistUpdateRequest"/>.</param>
+ /// <returns>Task.</returns>
+ Task UpdatePlaylist(PlaylistUpdateRequest request);
+
+ /// <summary>
/// Gets the playlists.
/// </summary>
/// <param name="userId">The user identifier.</param>
@@ -18,11 +41,20 @@ namespace MediaBrowser.Controller.Playlists
IEnumerable<Playlist> GetPlaylists(Guid userId);
/// <summary>
- /// Creates the playlist.
+ /// Adds a share to the playlist.
+ /// </summary>
+ /// <param name="request">The <see cref="PlaylistUserUpdateRequest"/>.</param>
+ /// <returns>Task.</returns>
+ Task AddUserToShares(PlaylistUserUpdateRequest request);
+
+ /// <summary>
+ /// Removes a share from the playlist.
/// </summary>
- /// <param name="options">The options.</param>
- /// <returns>Task&lt;Playlist&gt;.</returns>
- Task<PlaylistCreationResult> CreatePlaylist(PlaylistCreationRequest options);
+ /// <param name="playlistId">The playlist identifier.</param>
+ /// <param name="userId">The user identifier.</param>
+ /// <param name="share">The share.</param>
+ /// <returns>Task.</returns>
+ Task RemoveUserFromShares(Guid playlistId, Guid userId, PlaylistUserPermissions share);
/// <summary>
/// Adds to playlist.
@@ -31,7 +63,7 @@ namespace MediaBrowser.Controller.Playlists
/// <param name="itemIds">The item ids.</param>
/// <param name="userId">The user identifier.</param>
/// <returns>Task.</returns>
- Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId);
+ Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId);
/// <summary>
/// Removes from playlist.
@@ -39,7 +71,7 @@ namespace MediaBrowser.Controller.Playlists
/// <param name="playlistId">The playlist identifier.</param>
/// <param name="entryIds">The entry ids.</param>
/// <returns>Task.</returns>
- Task RemoveFromPlaylistAsync(string playlistId, IEnumerable<string> entryIds);
+ Task RemoveItemFromPlaylistAsync(string playlistId, IEnumerable<string> entryIds);
/// <summary>
/// Gets the playlists folder.
diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
index ca032e7f6..34b34e578 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -16,24 +16,23 @@ 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()
{
- Shares = Array.Empty<Share>();
+ Shares = [];
OpenAccess = false;
}
@@ -41,7 +40,7 @@ namespace MediaBrowser.Controller.Playlists
public bool OpenAccess { get; set; }
- public Share[] Shares { get; set; }
+ public IReadOnlyList<PlaylistUserPermissions> Shares { get; set; }
[JsonIgnore]
public bool IsFile => IsPlaylistFile(Path);
@@ -130,7 +129,7 @@ namespace MediaBrowser.Controller.Playlists
protected override List<BaseItem> LoadChildren()
{
// Save a trip to the database
- return new List<BaseItem>();
+ return [];
}
protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
@@ -145,7 +144,7 @@ namespace MediaBrowser.Controller.Playlists
protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
{
- return new List<BaseItem>();
+ return [];
}
public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
@@ -167,7 +166,7 @@ namespace MediaBrowser.Controller.Playlists
return base.GetChildren(user, true, query);
}
- public static List<BaseItem> GetPlaylistItems(MediaType playlistMediaType, IEnumerable<BaseItem> inputItems, User user, DtoOptions options)
+ public static IReadOnlyList<BaseItem> GetPlaylistItems(MediaType playlistMediaType, IEnumerable<BaseItem> inputItems, User user, DtoOptions options)
{
if (user is not null)
{
@@ -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,12 +247,17 @@ namespace MediaBrowser.Controller.Playlists
}
var shares = Shares;
- if (shares.Length == 0)
+ if (shares.Count == 0)
{
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 CanDelete(User user)
+ {
+ return user.HasPermission(PermissionKind.IsAdministrator) || user.Id.Equals(OwnerUserId);
}
public override bool IsVisibleStandalone(User user)
diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
index 8a870e0d9..a7e027d94 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<Share>();
+ var list = new List<PlaylistUserPermissions>();
reader.MoveToContent();
reader.Read();
@@ -565,7 +565,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
}
}
- item.Shares = list.ToArray();
+ item.Shares = [.. list];
}
/// <summary>
@@ -830,12 +830,12 @@ namespace MediaBrowser.LocalMetadata.Parsers
/// </summary>
/// <param name="reader">The xml reader.</param>
/// <returns>The share.</returns>
- protected Share? GetShare(XmlReader reader)
+ protected PlaylistUserPermissions? 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) && Guid.TryParse(userId, out var guid))
{
- return item;
+ 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/Entities/IHasShares.cs b/MediaBrowser.Model/Entities/IHasShares.cs
index b34d1a037..8c4ba6c42 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<PlaylistUserPermissions> 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;
+
+/// <summary>
+/// Class to hold data on user permissions for playlists.
+/// </summary>
+public class PlaylistUserPermissions
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PlaylistUserPermissions"/> class.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="canEdit">Edit permission.</param>
+ public PlaylistUserPermissions(Guid userId, bool canEdit = false)
+ {
+ UserId = userId;
+ CanEdit = canEdit;
+ }
+
+ /// <summary>
+ /// Gets or sets the user id.
+ /// </summary>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the user has edit permissions.
+ /// </summary>
+ public bool CanEdit { 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;
-
-/// <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; }
-}
diff --git a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs
index 62d496d04..ec54b1afd 100644
--- a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs
+++ b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs
@@ -18,7 +18,7 @@ public class PlaylistCreationRequest
/// <summary>
/// Gets or sets the list of items.
/// </summary>
- public IReadOnlyList<Guid> ItemIdList { get; set; } = Array.Empty<Guid>();
+ public IReadOnlyList<Guid> ItemIdList { get; set; } = [];
/// <summary>
/// Gets or sets the media type.
@@ -31,7 +31,12 @@ public class PlaylistCreationRequest
public Guid UserId { get; set; }
/// <summary>
- /// Gets or sets the shares.
+ /// Gets or sets the user permissions.
/// </summary>
- public Share[]? Shares { get; set; }
+ public IReadOnlyList<PlaylistUserPermissions> Users { get; set; } = [];
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the playlist is public.
+ /// </summary>
+ public bool? Public { get; set; } = true;
}
diff --git a/MediaBrowser.Model/Playlists/PlaylistUpdateRequest.cs b/MediaBrowser.Model/Playlists/PlaylistUpdateRequest.cs
new file mode 100644
index 000000000..db290bbdb
--- /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;
+
+/// <summary>
+/// A playlist update request.
+/// </summary>
+public class PlaylistUpdateRequest
+{
+ /// <summary>
+ /// Gets or sets the id of the playlist.
+ /// </summary>
+ public Guid Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id of the user updating the playlist.
+ /// </summary>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the playlist.
+ /// </summary>
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets item ids to add to the playlist.
+ /// </summary>
+ public IReadOnlyList<Guid>? Ids { get; set; }
+
+ /// <summary>
+ /// Gets or sets the playlist users.
+ /// </summary>
+ public IReadOnlyList<PlaylistUserPermissions>? Users { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the playlist is public.
+ /// </summary>
+ public bool? Public { get; set; }
+}
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;
+
+/// <summary>
+/// A playlist user update request.
+/// </summary>
+public class PlaylistUserUpdateRequest
+{
+ /// <summary>
+ /// Gets or sets the id of the playlist.
+ /// </summary>
+ public Guid Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id of the updated user.
+ /// </summary>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the user can edit the playlist.
+ /// </summary>
+ public bool? CanEdit { get; set; }
+}