< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.Playlists.PlaylistManager
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/Playlists/PlaylistManager.cs
Line coverage
9%
Covered lines: 14
Uncovered lines: 129
Coverable lines: 143
Total lines: 660
Line coverage: 9.7%
Branch coverage
6%
Covered branches: 5
Total branches: 72
Branch coverage: 6.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 8/14/2025 - 12:11:05 AM Line coverage: 9.7% (14/143) Branch coverage: 6.9% (5/72) Total lines: 6599/16/2025 - 12:09:46 AM Line coverage: 9.7% (14/143) Branch coverage: 8.3% (6/72) Total lines: 65911/18/2025 - 12:11:25 AM Line coverage: 9.7% (14/143) Branch coverage: 6.9% (5/72) Total lines: 660 8/14/2025 - 12:11:05 AM Line coverage: 9.7% (14/143) Branch coverage: 6.9% (5/72) Total lines: 6599/16/2025 - 12:09:46 AM Line coverage: 9.7% (14/143) Branch coverage: 8.3% (6/72) Total lines: 65911/18/2025 - 12:11:25 AM Line coverage: 9.7% (14/143) Branch coverage: 6.9% (5/72) Total lines: 660

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetPlaylistForUser(...)100%210%
GetPlaylists(...)100%210%
GetUserPlaylists(...)0%620%
GetTargetPath(...)0%620%
GetPlaylistItems(...)100%210%
AddItemToPlaylistAsync(...)0%620%
DetermineAdjustedIndex(...)100%44100%
SavePlaylistFile(...)0%2970540%
NormalizeItemPath(...)100%210%
MakeRelativePath(...)0%4260%
GetPlaylistsFolder()100%11100%
GetPlaylistsFolder(...)50%22100%

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/Playlists/PlaylistManager.cs

#LineLine coverage
 1#nullable disable
 2
 3#pragma warning disable CS1591
 4
 5using System;
 6using System.Collections.Generic;
 7using System.Globalization;
 8using System.IO;
 9using System.Linq;
 10using System.Threading;
 11using System.Threading.Tasks;
 12using Jellyfin.Data.Enums;
 13using Jellyfin.Database.Implementations.Entities;
 14using Jellyfin.Extensions;
 15using MediaBrowser.Controller.Dto;
 16using MediaBrowser.Controller.Entities;
 17using MediaBrowser.Controller.Entities.Audio;
 18using MediaBrowser.Controller.Extensions;
 19using MediaBrowser.Controller.Library;
 20using MediaBrowser.Controller.Playlists;
 21using MediaBrowser.Controller.Providers;
 22using MediaBrowser.Model.Entities;
 23using MediaBrowser.Model.IO;
 24using MediaBrowser.Model.Playlists;
 25using Microsoft.Extensions.Configuration;
 26using Microsoft.Extensions.Logging;
 27using PlaylistsNET.Content;
 28using PlaylistsNET.Models;
 29using Genre = MediaBrowser.Controller.Entities.Genre;
 30using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
 31
 32namespace Emby.Server.Implementations.Playlists
 33{
 34    public class PlaylistManager : IPlaylistManager
 35    {
 36        private readonly ILibraryManager _libraryManager;
 37        private readonly IFileSystem _fileSystem;
 38        private readonly ILibraryMonitor _iLibraryMonitor;
 39        private readonly ILogger<PlaylistManager> _logger;
 40        private readonly IUserManager _userManager;
 41        private readonly IProviderManager _providerManager;
 42        private readonly IConfiguration _appConfig;
 43
 44        public PlaylistManager(
 45            ILibraryManager libraryManager,
 46            IFileSystem fileSystem,
 47            ILibraryMonitor iLibraryMonitor,
 48            ILogger<PlaylistManager> logger,
 49            IUserManager userManager,
 50            IProviderManager providerManager,
 51            IConfiguration appConfig)
 52        {
 2153            _libraryManager = libraryManager;
 2154            _fileSystem = fileSystem;
 2155            _iLibraryMonitor = iLibraryMonitor;
 2156            _logger = logger;
 2157            _userManager = userManager;
 2158            _providerManager = providerManager;
 2159            _appConfig = appConfig;
 2160        }
 61
 62        public Playlist GetPlaylistForUser(Guid playlistId, Guid userId)
 63        {
 064            return GetPlaylists(userId).Where(p => p.Id.Equals(playlistId)).FirstOrDefault();
 65        }
 66
 67        public IEnumerable<Playlist> GetPlaylists(Guid userId)
 68        {
 069            var user = _userManager.GetUserById(userId);
 070            return _libraryManager.GetItemList(new InternalItemsQuery
 071            {
 072                IncludeItemTypes = [BaseItemKind.Playlist],
 073                Recursive = true,
 074                DtoOptions = new DtoOptions(false)
 075            })
 076            .Cast<Playlist>()
 077            .Where(p => p.IsVisible(user));
 78        }
 79
 80        public async Task<PlaylistCreationResult> CreatePlaylist(PlaylistCreationRequest request)
 81        {
 82            var name = request.Name;
 83            var folderName = _fileSystem.GetValidFilename(name);
 84            var parentFolder = GetPlaylistsFolder(request.UserId);
 85            if (parentFolder is null)
 86            {
 87                throw new ArgumentException(nameof(parentFolder));
 88            }
 89
 90            if (request.MediaType is null || request.MediaType == MediaType.Unknown)
 91            {
 92                foreach (var itemId in request.ItemIdList)
 93                {
 94                    var item = _libraryManager.GetItemById(itemId) ?? throw new ArgumentException("No item exists with t
 95                    if (item.MediaType != MediaType.Unknown)
 96                    {
 97                        request.MediaType = item.MediaType;
 98                    }
 99                    else if (item is MusicArtist || item is MusicAlbum || item is MusicGenre)
 100                    {
 101                        request.MediaType = MediaType.Audio;
 102                    }
 103                    else if (item is Genre)
 104                    {
 105                        request.MediaType = MediaType.Video;
 106                    }
 107                    else
 108                    {
 109                        if (item is Folder folder)
 110                        {
 111                            request.MediaType = folder.GetRecursiveChildren(i => !i.IsFolder && i.SupportsAddingToPlayli
 112                                .Select(i => i.MediaType)
 113                                .FirstOrDefault(i => i != MediaType.Unknown);
 114                        }
 115                    }
 116
 117                    if (request.MediaType is not null && request.MediaType != MediaType.Unknown)
 118                    {
 119                        break;
 120                    }
 121                }
 122            }
 123
 124            if (request.MediaType is null || request.MediaType == MediaType.Unknown)
 125            {
 126                request.MediaType = MediaType.Audio;
 127            }
 128
 129            var user = _userManager.GetUserById(request.UserId);
 130            var path = Path.Combine(parentFolder.Path, folderName);
 131            path = GetTargetPath(path);
 132
 133            _iLibraryMonitor.ReportFileSystemChangeBeginning(path);
 134
 135            try
 136            {
 137                var info = Directory.CreateDirectory(path);
 138                var playlist = new Playlist
 139                {
 140                    Name = name,
 141                    Path = path,
 142                    OwnerUserId = request.UserId,
 143                    Shares = request.Users ?? [],
 144                    OpenAccess = request.Public ?? false,
 145                    DateCreated = info.CreationTimeUtc,
 146                    DateModified = info.LastWriteTimeUtc
 147                };
 148
 149                playlist.SetMediaType(request.MediaType);
 150                parentFolder.AddChild(playlist);
 151
 152                await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave
 153                    .ConfigureAwait(false);
 154
 155                if (request.ItemIdList.Count > 0)
 156                {
 157                    await AddToPlaylistInternal(playlist.Id, request.ItemIdList, user, new DtoOptions(false)
 158                    {
 159                        EnableImages = true
 160                    }).ConfigureAwait(false);
 161                }
 162
 163                return new PlaylistCreationResult(playlist.Id.ToString("N", CultureInfo.InvariantCulture));
 164            }
 165            finally
 166            {
 167                // Refresh handled internally
 168                _iLibraryMonitor.ReportFileSystemChangeComplete(path, false);
 169            }
 170        }
 171
 172        private List<Playlist> GetUserPlaylists(Guid userId)
 173        {
 0174            var user = _userManager.GetUserById(userId);
 0175            var playlistsFolder = GetPlaylistsFolder(userId);
 0176            if (playlistsFolder is null)
 177            {
 0178                return [];
 179            }
 180
 0181            return playlistsFolder.GetChildren(user, true).OfType<Playlist>().ToList();
 182        }
 183
 184        private static string GetTargetPath(string path)
 185        {
 0186            while (Directory.Exists(path))
 187            {
 0188                path += "1";
 189            }
 190
 0191            return path;
 192        }
 193
 194        private IReadOnlyList<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, User user, DtoOptions options)
 195        {
 0196            var items = itemIds.Select(_libraryManager.GetItemById).Where(i => i is not null);
 197
 0198            return Playlist.GetPlaylistItems(items, user, options);
 199        }
 200
 201        public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
 202        {
 0203            var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
 204
 0205            return AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false)
 0206            {
 0207                EnableImages = true
 0208            });
 209        }
 210
 211        private async Task AddToPlaylistInternal(Guid playlistId, IReadOnlyCollection<Guid> newItemIds, User user, DtoOp
 212        {
 213            // Retrieve the existing playlist
 214            var playlist = _libraryManager.GetItemById(playlistId) as Playlist
 215                ?? throw new ArgumentException("No Playlist exists with Id " + playlistId);
 216
 217            // Retrieve all the items to be added to the playlist
 218            var newItems = GetPlaylistItems(newItemIds, user, options)
 219                .Where(i => i.SupportsAddingToPlaylist);
 220
 221            // Filter out duplicate items
 222            var existingIds = playlist.LinkedChildren.Select(c => c.ItemId).ToHashSet();
 223            newItems = newItems
 224                .Where(i => !existingIds.Contains(i.Id))
 225                .Distinct();
 226
 227            // Create a list of the new linked children to add to the playlist
 228            var childrenToAdd = newItems
 229                .Select(LinkedChild.Create)
 230                .ToList();
 231
 232            // Log duplicates that have been ignored, if any
 233            int numDuplicates = newItemIds.Count - childrenToAdd.Count;
 234            if (numDuplicates > 0)
 235            {
 236                _logger.LogWarning("Ignored adding {DuplicateCount} duplicate items to playlist {PlaylistName}.", numDup
 237            }
 238
 239            // Do nothing else if there are no items to add to the playlist
 240            if (childrenToAdd.Count == 0)
 241            {
 242                return;
 243            }
 244
 245            // Update the playlist in the repository
 246            playlist.LinkedChildren = [.. playlist.LinkedChildren, .. childrenToAdd];
 247            playlist.DateLastMediaAdded = DateTime.UtcNow;
 248
 249            await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
 250
 251            // Refresh playlist metadata
 252            _providerManager.QueueRefresh(
 253                playlist.Id,
 254                new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 255                {
 256                    ForceSave = true
 257                },
 258                RefreshPriority.High);
 259        }
 260
 261        public async Task RemoveItemFromPlaylistAsync(string playlistId, IEnumerable<string> entryIds)
 262        {
 263            if (_libraryManager.GetItemById(playlistId) is not Playlist playlist)
 264            {
 265                throw new ArgumentException("No Playlist exists with the supplied Id");
 266            }
 267
 268            var children = playlist.GetManageableItems().ToList();
 269
 270            var idList = entryIds.ToList();
 271
 272            var removals = children.Where(i => idList.Contains(i.Item1.ItemId?.ToString("N", CultureInfo.InvariantCultur
 273
 274            playlist.LinkedChildren = children.Except(removals)
 275                .Select(i => i.Item1)
 276                .ToArray();
 277
 278            await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
 279
 280            _providerManager.QueueRefresh(
 281                playlist.Id,
 282                new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 283                {
 284                    ForceSave = true
 285                },
 286                RefreshPriority.High);
 287        }
 288
 289        internal static int DetermineAdjustedIndex(int newPriorIndexAllChildren, int newIndex)
 290        {
 3291            if (newIndex == 0)
 292            {
 2293                return newPriorIndexAllChildren > 0 ? newPriorIndexAllChildren - 1 : 0;
 294            }
 295
 1296            return newPriorIndexAllChildren + 1;
 297        }
 298
 299        public async Task MoveItemAsync(string playlistId, string entryId, int newIndex, Guid callingUserId)
 300        {
 301            if (_libraryManager.GetItemById(playlistId) is not Playlist playlist)
 302            {
 303                throw new ArgumentException("No Playlist exists with the supplied Id");
 304            }
 305
 306            var user = _userManager.GetUserById(callingUserId);
 307            var children = playlist.GetManageableItems().ToList();
 308            var accessibleChildren = children.Where(c => c.Item2.IsVisible(user)).ToArray();
 309
 310            var oldIndexAll = children.FindIndex(i => string.Equals(entryId, i.Item1.ItemId?.ToString("N", CultureInfo.I
 311            var oldIndexAccessible = accessibleChildren.FindIndex(i => string.Equals(entryId, i.Item1.ItemId?.ToString("
 312
 313            if (oldIndexAccessible == newIndex)
 314            {
 315                return;
 316            }
 317
 318            var newPriorItemIndex = Math.Max(newIndex - 1, 0);
 319            var newPriorItemId = accessibleChildren[newPriorItemIndex].Item1.ItemId;
 320            var newPriorItemIndexOnAllChildren = children.FindIndex(c => c.Item1.ItemId.Equals(newPriorItemId));
 321            var adjustedNewIndex = DetermineAdjustedIndex(newPriorItemIndexOnAllChildren, newIndex);
 322
 323            var item = playlist.LinkedChildren.FirstOrDefault(i => string.Equals(entryId, i.ItemId?.ToString("N", Cultur
 324            if (item is null)
 325            {
 326                _logger.LogWarning("Modified item not found in playlist. ItemId: {ItemId}, PlaylistId: {PlaylistId}", en
 327
 328                return;
 329            }
 330
 331            var newList = playlist.LinkedChildren.ToList();
 332            newList.Remove(item);
 333
 334            if (newIndex >= newList.Count)
 335            {
 336                newList.Add(item);
 337            }
 338            else
 339            {
 340                newList.Insert(adjustedNewIndex, item);
 341            }
 342
 343            playlist.LinkedChildren = [.. newList];
 344
 345            await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
 346        }
 347
 348        /// <inheritdoc />
 349        public void SavePlaylistFile(Playlist item)
 350        {
 351            // this is probably best done as a metadata provider
 352            // saving a file over itself will require some work to prevent this from happening when not needed
 0353            var playlistPath = item.Path;
 0354            var extension = Path.GetExtension(playlistPath.AsSpan());
 355
 0356            if (extension.Equals(".wpl", StringComparison.OrdinalIgnoreCase))
 357            {
 0358                var playlist = new WplPlaylist();
 0359                foreach (var child in item.GetLinkedChildren())
 360                {
 0361                    var entry = new WplPlaylistEntry()
 0362                    {
 0363                        Path = NormalizeItemPath(playlistPath, child.Path),
 0364                        TrackTitle = child.Name,
 0365                        AlbumTitle = child.Album
 0366                    };
 367
 0368                    if (child is IHasAlbumArtist hasAlbumArtist)
 369                    {
 0370                        entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : nul
 371                    }
 372
 0373                    if (child is IHasArtist hasArtist)
 374                    {
 0375                        entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
 376                    }
 377
 0378                    if (child.RunTimeTicks.HasValue)
 379                    {
 0380                        entry.Duration = TimeSpan.FromTicks(child.RunTimeTicks.Value);
 381                    }
 382
 0383                    playlist.PlaylistEntries.Add(entry);
 384                }
 385
 0386                string text = new WplContent().ToText(playlist);
 0387                File.WriteAllText(playlistPath, text);
 388            }
 0389            else if (extension.Equals(".zpl", StringComparison.OrdinalIgnoreCase))
 390            {
 0391                var playlist = new ZplPlaylist();
 0392                foreach (var child in item.GetLinkedChildren())
 393                {
 0394                    var entry = new ZplPlaylistEntry()
 0395                    {
 0396                        Path = NormalizeItemPath(playlistPath, child.Path),
 0397                        TrackTitle = child.Name,
 0398                        AlbumTitle = child.Album
 0399                    };
 400
 0401                    if (child is IHasAlbumArtist hasAlbumArtist)
 402                    {
 0403                        entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : nul
 404                    }
 405
 0406                    if (child is IHasArtist hasArtist)
 407                    {
 0408                        entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
 409                    }
 410
 0411                    if (child.RunTimeTicks.HasValue)
 412                    {
 0413                        entry.Duration = TimeSpan.FromTicks(child.RunTimeTicks.Value);
 414                    }
 415
 0416                    playlist.PlaylistEntries.Add(entry);
 417                }
 418
 0419                string text = new ZplContent().ToText(playlist);
 0420                File.WriteAllText(playlistPath, text);
 421            }
 0422            else if (extension.Equals(".m3u", StringComparison.OrdinalIgnoreCase))
 423            {
 0424                var playlist = new M3uPlaylist
 0425                {
 0426                    IsExtended = true
 0427                };
 0428                foreach (var child in item.GetLinkedChildren())
 429                {
 0430                    var entry = new M3uPlaylistEntry()
 0431                    {
 0432                        Path = NormalizeItemPath(playlistPath, child.Path),
 0433                        Title = child.Name,
 0434                        Album = child.Album
 0435                    };
 436
 0437                    if (child is IHasAlbumArtist hasAlbumArtist)
 438                    {
 0439                        entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : nul
 440                    }
 441
 0442                    if (child.RunTimeTicks.HasValue)
 443                    {
 0444                        entry.Duration = TimeSpan.FromTicks(child.RunTimeTicks.Value);
 445                    }
 446
 0447                    playlist.PlaylistEntries.Add(entry);
 448                }
 449
 0450                string text = new M3uContent().ToText(playlist);
 0451                File.WriteAllText(playlistPath, text);
 452            }
 0453            else if (extension.Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
 454            {
 0455                var playlist = new M3uPlaylist
 0456                {
 0457                    IsExtended = true
 0458                };
 459
 0460                foreach (var child in item.GetLinkedChildren())
 461                {
 0462                    var entry = new M3uPlaylistEntry()
 0463                    {
 0464                        Path = NormalizeItemPath(playlistPath, child.Path),
 0465                        Title = child.Name,
 0466                        Album = child.Album
 0467                    };
 468
 0469                    if (child is IHasAlbumArtist hasAlbumArtist)
 470                    {
 0471                        entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : nul
 472                    }
 473
 0474                    if (child.RunTimeTicks.HasValue)
 475                    {
 0476                        entry.Duration = TimeSpan.FromTicks(child.RunTimeTicks.Value);
 477                    }
 478
 0479                    playlist.PlaylistEntries.Add(entry);
 480                }
 481
 0482                string text = new M3uContent().ToText(playlist);
 0483                File.WriteAllText(playlistPath, text);
 484            }
 0485            else if (extension.Equals(".pls", StringComparison.OrdinalIgnoreCase))
 486            {
 0487                var playlist = new PlsPlaylist();
 0488                foreach (var child in item.GetLinkedChildren())
 489                {
 0490                    var entry = new PlsPlaylistEntry()
 0491                    {
 0492                        Path = NormalizeItemPath(playlistPath, child.Path),
 0493                        Title = child.Name
 0494                    };
 495
 0496                    if (child.RunTimeTicks.HasValue)
 497                    {
 0498                        entry.Length = TimeSpan.FromTicks(child.RunTimeTicks.Value);
 499                    }
 500
 0501                    playlist.PlaylistEntries.Add(entry);
 502                }
 503
 0504                string text = new PlsContent().ToText(playlist);
 0505                File.WriteAllText(playlistPath, text);
 506            }
 0507        }
 508
 509        private static string NormalizeItemPath(string playlistPath, string itemPath)
 510        {
 0511            return MakeRelativePath(Path.GetDirectoryName(playlistPath), itemPath);
 512        }
 513
 514        private static string MakeRelativePath(string folderPath, string fileAbsolutePath)
 515        {
 0516            ArgumentException.ThrowIfNullOrEmpty(folderPath);
 0517            ArgumentException.ThrowIfNullOrEmpty(fileAbsolutePath);
 518
 0519            if (!folderPath.EndsWith(Path.DirectorySeparatorChar))
 520            {
 0521                folderPath += Path.DirectorySeparatorChar;
 522            }
 523
 0524            var folderUri = new Uri(folderPath);
 0525            var fileAbsoluteUri = new Uri(fileAbsolutePath);
 526
 527            // path can't be made relative
 0528            if (folderUri.Scheme != fileAbsoluteUri.Scheme)
 529            {
 0530                return fileAbsolutePath;
 531            }
 532
 0533            var relativeUri = folderUri.MakeRelativeUri(fileAbsoluteUri);
 0534            string relativePath = Uri.UnescapeDataString(relativeUri.ToString());
 535
 0536            if (fileAbsoluteUri.Scheme.Equals("file", StringComparison.OrdinalIgnoreCase))
 537            {
 0538                relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
 539            }
 540
 0541            return relativePath;
 542        }
 543
 544        /// <inheritdoc />
 545        public Folder GetPlaylistsFolder()
 546        {
 2547            return GetPlaylistsFolder(Guid.Empty);
 548        }
 549
 550        /// <inheritdoc />
 551        public Folder GetPlaylistsFolder(Guid userId)
 552        {
 553            const string TypeName = "PlaylistsFolder";
 554
 2555            return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Na
 2556                _libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetTyp
 557        }
 558
 559        /// <inheritdoc />
 560        public async Task RemovePlaylistsAsync(Guid userId)
 561        {
 562            var playlists = GetUserPlaylists(userId);
 563            foreach (var playlist in playlists)
 564            {
 565                // Update owner if shared
 566                var rankedShares = playlist.Shares.OrderByDescending(x => x.CanEdit).ToList();
 567                if (rankedShares.Count > 0)
 568                {
 569                    playlist.OwnerUserId = rankedShares[0].UserId;
 570                    playlist.Shares = rankedShares.Skip(1).ToArray();
 571                    await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
 572                }
 573                else if (!playlist.OpenAccess)
 574                {
 575                    // Remove playlist if not shared
 576                    _libraryManager.DeleteItem(
 577                        playlist,
 578                        new DeleteOptions
 579                        {
 580                            DeleteFileLocation = false,
 581                            DeleteFromExternalProvider = false
 582                        },
 583                        playlist.GetParent(),
 584                        false);
 585                }
 586            }
 587        }
 588
 589        public async Task UpdatePlaylist(PlaylistUpdateRequest request)
 590        {
 591            var playlist = GetPlaylistForUser(request.Id, request.UserId);
 592
 593            if (request.Ids is not null)
 594            {
 595                playlist.LinkedChildren = [];
 596                await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
 597
 598                var user = _userManager.GetUserById(request.UserId);
 599                await AddToPlaylistInternal(request.Id, request.Ids, user, new DtoOptions(false)
 600                {
 601                    EnableImages = true
 602                }).ConfigureAwait(false);
 603
 604                playlist = GetPlaylistForUser(request.Id, request.UserId);
 605            }
 606
 607            if (request.Name is not null)
 608            {
 609                playlist.Name = request.Name;
 610            }
 611
 612            if (request.Users is not null)
 613            {
 614                playlist.Shares = request.Users;
 615            }
 616
 617            if (request.Public is not null)
 618            {
 619                playlist.OpenAccess = request.Public.Value;
 620            }
 621
 622            await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
 623        }
 624
 625        public async Task AddUserToShares(PlaylistUserUpdateRequest request)
 626        {
 627            var userId = request.UserId;
 628            var playlist = GetPlaylistForUser(request.Id, userId);
 629            var shares = playlist.Shares.ToList();
 630            var existingUserShare = shares.FirstOrDefault(s => s.UserId.Equals(userId));
 631            if (existingUserShare is not null)
 632            {
 633                shares.Remove(existingUserShare);
 634            }
 635
 636            shares.Add(new PlaylistUserPermissions(userId, request.CanEdit ?? false));
 637            playlist.Shares = shares;
 638            await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
 639        }
 640
 641        public async Task RemoveUserFromShares(Guid playlistId, Guid userId, PlaylistUserPermissions share)
 642        {
 643            var playlist = GetPlaylistForUser(playlistId, userId);
 644            var shares = playlist.Shares.ToList();
 645            shares.Remove(share);
 646            playlist.Shares = shares;
 647            await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
 648        }
 649
 650        private async Task UpdatePlaylistInternal(Playlist playlist)
 651        {
 652            await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(f
 653
 654            if (playlist.IsFile)
 655            {
 656                SavePlaylistFile(playlist);
 657            }
 658        }
 659    }
 660}