mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-12-27 09:56:33 +00:00
Merge pull request #11220 from Shadowghost/add-playlist-acl-api
Add playlist ACL endpoints
This commit is contained in:
commit
ee1d6332ee
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,12 +92,232 @@ 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>
|
||||
|
@ -105,16 +325,34 @@ public class PlaylistsController : BaseJellyfinApiController
|
|||
/// <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()
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
34
Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs
Normal file
34
Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs
Normal file
|
@ -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; }
|
||||
}
|
12
Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistUserDto.cs
Normal file
12
Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistUserDto.cs
Normal file
|
@ -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; }
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -4,12 +4,35 @@ 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="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>
|
||||
|
@ -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="options">The options.</param>
|
||||
/// <returns>Task<Playlist>.</returns>
|
||||
Task<PlaylistCreationResult> CreatePlaylist(PlaylistCreationRequest options);
|
||||
/// <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="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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
30
MediaBrowser.Model/Entities/PlaylistUserPermissions.cs
Normal file
30
MediaBrowser.Model/Entities/PlaylistUserPermissions.cs
Normal file
|
@ -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; }
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
41
MediaBrowser.Model/Playlists/PlaylistUpdateRequest.cs
Normal file
41
MediaBrowser.Model/Playlists/PlaylistUpdateRequest.cs
Normal file
|
@ -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; }
|
||||
}
|
24
MediaBrowser.Model/Playlists/PlaylistUserUpdateRequest.cs
Normal file
24
MediaBrowser.Model/Playlists/PlaylistUserUpdateRequest.cs
Normal file
|
@ -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; }
|
||||
}
|
Loading…
Reference in a new issue