PlaylistManager.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. #nullable disable
  2. #pragma warning disable CS1591
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Globalization;
  6. using System.IO;
  7. using System.Linq;
  8. using System.Threading;
  9. using System.Threading.Tasks;
  10. using Jellyfin.Data.Entities;
  11. using MediaBrowser.Controller.Dto;
  12. using MediaBrowser.Controller.Entities;
  13. using MediaBrowser.Controller.Entities.Audio;
  14. using MediaBrowser.Controller.Extensions;
  15. using MediaBrowser.Controller.Library;
  16. using MediaBrowser.Controller.Playlists;
  17. using MediaBrowser.Controller.Providers;
  18. using MediaBrowser.Model.Entities;
  19. using MediaBrowser.Model.IO;
  20. using MediaBrowser.Model.Playlists;
  21. using Microsoft.Extensions.Configuration;
  22. using Microsoft.Extensions.Logging;
  23. using PlaylistsNET.Content;
  24. using PlaylistsNET.Models;
  25. using Genre = MediaBrowser.Controller.Entities.Genre;
  26. using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
  27. namespace Emby.Server.Implementations.Playlists
  28. {
  29. public class PlaylistManager : IPlaylistManager
  30. {
  31. private readonly ILibraryManager _libraryManager;
  32. private readonly IFileSystem _fileSystem;
  33. private readonly ILibraryMonitor _iLibraryMonitor;
  34. private readonly ILogger<PlaylistManager> _logger;
  35. private readonly IUserManager _userManager;
  36. private readonly IProviderManager _providerManager;
  37. private readonly IConfiguration _appConfig;
  38. public PlaylistManager(
  39. ILibraryManager libraryManager,
  40. IFileSystem fileSystem,
  41. ILibraryMonitor iLibraryMonitor,
  42. ILogger<PlaylistManager> logger,
  43. IUserManager userManager,
  44. IProviderManager providerManager,
  45. IConfiguration appConfig)
  46. {
  47. _libraryManager = libraryManager;
  48. _fileSystem = fileSystem;
  49. _iLibraryMonitor = iLibraryMonitor;
  50. _logger = logger;
  51. _userManager = userManager;
  52. _providerManager = providerManager;
  53. _appConfig = appConfig;
  54. }
  55. public IEnumerable<Playlist> GetPlaylists(Guid userId)
  56. {
  57. var user = _userManager.GetUserById(userId);
  58. return GetPlaylistsFolder(userId).GetChildren(user, true).OfType<Playlist>();
  59. }
  60. public async Task<PlaylistCreationResult> CreatePlaylist(PlaylistCreationRequest options)
  61. {
  62. var name = options.Name;
  63. var folderName = _fileSystem.GetValidFilename(name);
  64. var parentFolder = GetPlaylistsFolder(Guid.Empty);
  65. if (parentFolder == null)
  66. {
  67. throw new ArgumentException(nameof(parentFolder));
  68. }
  69. if (string.IsNullOrEmpty(options.MediaType))
  70. {
  71. foreach (var itemId in options.ItemIdList)
  72. {
  73. var item = _libraryManager.GetItemById(itemId);
  74. if (item == null)
  75. {
  76. throw new ArgumentException("No item exists with the supplied Id");
  77. }
  78. if (!string.IsNullOrEmpty(item.MediaType))
  79. {
  80. options.MediaType = item.MediaType;
  81. }
  82. else if (item is MusicArtist || item is MusicAlbum || item is MusicGenre)
  83. {
  84. options.MediaType = MediaType.Audio;
  85. }
  86. else if (item is Genre)
  87. {
  88. options.MediaType = MediaType.Video;
  89. }
  90. else
  91. {
  92. if (item is Folder folder)
  93. {
  94. options.MediaType = folder.GetRecursiveChildren(i => !i.IsFolder && i.SupportsAddingToPlaylist)
  95. .Select(i => i.MediaType)
  96. .FirstOrDefault(i => !string.IsNullOrEmpty(i));
  97. }
  98. }
  99. if (!string.IsNullOrEmpty(options.MediaType))
  100. {
  101. break;
  102. }
  103. }
  104. }
  105. if (string.IsNullOrEmpty(options.MediaType))
  106. {
  107. options.MediaType = "Audio";
  108. }
  109. var user = _userManager.GetUserById(options.UserId);
  110. var path = Path.Combine(parentFolder.Path, folderName);
  111. path = GetTargetPath(path);
  112. _iLibraryMonitor.ReportFileSystemChangeBeginning(path);
  113. try
  114. {
  115. Directory.CreateDirectory(path);
  116. var playlist = new Playlist
  117. {
  118. Name = name,
  119. Path = path,
  120. Shares = new[]
  121. {
  122. new Share
  123. {
  124. UserId = options.UserId.Equals(default)
  125. ? null
  126. : options.UserId.ToString("N", CultureInfo.InvariantCulture),
  127. CanEdit = true
  128. }
  129. }
  130. };
  131. playlist.SetMediaType(options.MediaType);
  132. parentFolder.AddChild(playlist);
  133. await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, CancellationToken.None)
  134. .ConfigureAwait(false);
  135. if (options.ItemIdList.Count > 0)
  136. {
  137. await AddToPlaylistInternal(playlist.Id, options.ItemIdList, user, new DtoOptions(false)
  138. {
  139. EnableImages = true
  140. }).ConfigureAwait(false);
  141. }
  142. return new PlaylistCreationResult(playlist.Id.ToString("N", CultureInfo.InvariantCulture));
  143. }
  144. finally
  145. {
  146. // Refresh handled internally
  147. _iLibraryMonitor.ReportFileSystemChangeComplete(path, false);
  148. }
  149. }
  150. private string GetTargetPath(string path)
  151. {
  152. while (Directory.Exists(path))
  153. {
  154. path += "1";
  155. }
  156. return path;
  157. }
  158. private List<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, string playlistMediaType, User user, DtoOptions options)
  159. {
  160. var items = itemIds.Select(i => _libraryManager.GetItemById(i)).Where(i => i != null);
  161. return Playlist.GetPlaylistItems(playlistMediaType, items, user, options);
  162. }
  163. public Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
  164. {
  165. var user = userId.Equals(default) ? null : _userManager.GetUserById(userId);
  166. return AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false)
  167. {
  168. EnableImages = true
  169. });
  170. }
  171. private async Task AddToPlaylistInternal(Guid playlistId, IReadOnlyCollection<Guid> newItemIds, User user, DtoOptions options)
  172. {
  173. // Retrieve the existing playlist
  174. var playlist = _libraryManager.GetItemById(playlistId) as Playlist
  175. ?? throw new ArgumentException("No Playlist exists with Id " + playlistId);
  176. // Retrieve all the items to be added to the playlist
  177. var newItems = GetPlaylistItems(newItemIds, playlist.MediaType, user, options)
  178. .Where(i => i.SupportsAddingToPlaylist);
  179. // Filter out duplicate items, if necessary
  180. if (!_appConfig.DoPlaylistsAllowDuplicates())
  181. {
  182. var existingIds = playlist.LinkedChildren.Select(c => c.ItemId).ToHashSet();
  183. newItems = newItems
  184. .Where(i => !existingIds.Contains(i.Id))
  185. .Distinct();
  186. }
  187. // Create a list of the new linked children to add to the playlist
  188. var childrenToAdd = newItems
  189. .Select(LinkedChild.Create)
  190. .ToList();
  191. // Log duplicates that have been ignored, if any
  192. int numDuplicates = newItemIds.Count - childrenToAdd.Count;
  193. if (numDuplicates > 0)
  194. {
  195. _logger.LogWarning("Ignored adding {DuplicateCount} duplicate items to playlist {PlaylistName}.", numDuplicates, playlist.Name);
  196. }
  197. // Do nothing else if there are no items to add to the playlist
  198. if (childrenToAdd.Count == 0)
  199. {
  200. return;
  201. }
  202. // Create a new array with the updated playlist items
  203. var newLinkedChildren = new LinkedChild[playlist.LinkedChildren.Length + childrenToAdd.Count];
  204. playlist.LinkedChildren.CopyTo(newLinkedChildren, 0);
  205. childrenToAdd.CopyTo(newLinkedChildren, playlist.LinkedChildren.Length);
  206. // Update the playlist in the repository
  207. playlist.LinkedChildren = newLinkedChildren;
  208. await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
  209. // Update the playlist on disk
  210. if (playlist.IsFile)
  211. {
  212. SavePlaylistFile(playlist);
  213. }
  214. // Refresh playlist metadata
  215. _providerManager.QueueRefresh(
  216. playlist.Id,
  217. new MetadataRefreshOptions(new DirectoryService(_fileSystem))
  218. {
  219. ForceSave = true
  220. },
  221. RefreshPriority.High);
  222. }
  223. public async Task RemoveFromPlaylistAsync(string playlistId, IEnumerable<string> entryIds)
  224. {
  225. if (_libraryManager.GetItemById(playlistId) is not Playlist playlist)
  226. {
  227. throw new ArgumentException("No Playlist exists with the supplied Id");
  228. }
  229. var children = playlist.GetManageableItems().ToList();
  230. var idList = entryIds.ToList();
  231. var removals = children.Where(i => idList.Contains(i.Item1.Id));
  232. playlist.LinkedChildren = children.Except(removals)
  233. .Select(i => i.Item1)
  234. .ToArray();
  235. await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
  236. if (playlist.IsFile)
  237. {
  238. SavePlaylistFile(playlist);
  239. }
  240. _providerManager.QueueRefresh(
  241. playlist.Id,
  242. new MetadataRefreshOptions(new DirectoryService(_fileSystem))
  243. {
  244. ForceSave = true
  245. },
  246. RefreshPriority.High);
  247. }
  248. public async Task MoveItemAsync(string playlistId, string entryId, int newIndex)
  249. {
  250. if (_libraryManager.GetItemById(playlistId) is not Playlist playlist)
  251. {
  252. throw new ArgumentException("No Playlist exists with the supplied Id");
  253. }
  254. var children = playlist.GetManageableItems().ToList();
  255. var oldIndex = children.FindIndex(i => string.Equals(entryId, i.Item1.Id, StringComparison.OrdinalIgnoreCase));
  256. if (oldIndex == newIndex)
  257. {
  258. return;
  259. }
  260. var item = playlist.LinkedChildren[oldIndex];
  261. var newList = playlist.LinkedChildren.ToList();
  262. newList.Remove(item);
  263. if (newIndex >= newList.Count)
  264. {
  265. newList.Add(item);
  266. }
  267. else
  268. {
  269. newList.Insert(newIndex, item);
  270. }
  271. playlist.LinkedChildren = newList.ToArray();
  272. await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
  273. if (playlist.IsFile)
  274. {
  275. SavePlaylistFile(playlist);
  276. }
  277. }
  278. private void SavePlaylistFile(Playlist item)
  279. {
  280. // this is probably best done as a metadata provider
  281. // saving a file over itself will require some work to prevent this from happening when not needed
  282. var playlistPath = item.Path;
  283. var extension = Path.GetExtension(playlistPath);
  284. if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase))
  285. {
  286. var playlist = new WplPlaylist();
  287. foreach (var child in item.GetLinkedChildren())
  288. {
  289. var entry = new WplPlaylistEntry()
  290. {
  291. Path = NormalizeItemPath(playlistPath, child.Path),
  292. TrackTitle = child.Name,
  293. AlbumTitle = child.Album
  294. };
  295. if (child is IHasAlbumArtist hasAlbumArtist)
  296. {
  297. entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
  298. }
  299. if (child is IHasArtist hasArtist)
  300. {
  301. entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
  302. }
  303. if (child.RunTimeTicks.HasValue)
  304. {
  305. entry.Duration = TimeSpan.FromTicks(child.RunTimeTicks.Value);
  306. }
  307. playlist.PlaylistEntries.Add(entry);
  308. }
  309. string text = new WplContent().ToText(playlist);
  310. File.WriteAllText(playlistPath, text);
  311. }
  312. if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
  313. {
  314. var playlist = new ZplPlaylist();
  315. foreach (var child in item.GetLinkedChildren())
  316. {
  317. var entry = new ZplPlaylistEntry()
  318. {
  319. Path = NormalizeItemPath(playlistPath, child.Path),
  320. TrackTitle = child.Name,
  321. AlbumTitle = child.Album
  322. };
  323. if (child is IHasAlbumArtist hasAlbumArtist)
  324. {
  325. entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
  326. }
  327. if (child is IHasArtist hasArtist)
  328. {
  329. entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
  330. }
  331. if (child.RunTimeTicks.HasValue)
  332. {
  333. entry.Duration = TimeSpan.FromTicks(child.RunTimeTicks.Value);
  334. }
  335. playlist.PlaylistEntries.Add(entry);
  336. }
  337. string text = new ZplContent().ToText(playlist);
  338. File.WriteAllText(playlistPath, text);
  339. }
  340. if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
  341. {
  342. var playlist = new M3uPlaylist
  343. {
  344. IsExtended = true
  345. };
  346. foreach (var child in item.GetLinkedChildren())
  347. {
  348. var entry = new M3uPlaylistEntry()
  349. {
  350. Path = NormalizeItemPath(playlistPath, child.Path),
  351. Title = child.Name,
  352. Album = child.Album
  353. };
  354. if (child is IHasAlbumArtist hasAlbumArtist)
  355. {
  356. entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
  357. }
  358. if (child.RunTimeTicks.HasValue)
  359. {
  360. entry.Duration = TimeSpan.FromTicks(child.RunTimeTicks.Value);
  361. }
  362. playlist.PlaylistEntries.Add(entry);
  363. }
  364. string text = new M3uContent().ToText(playlist);
  365. File.WriteAllText(playlistPath, text);
  366. }
  367. if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
  368. {
  369. var playlist = new M3uPlaylist();
  370. playlist.IsExtended = true;
  371. foreach (var child in item.GetLinkedChildren())
  372. {
  373. var entry = new M3uPlaylistEntry()
  374. {
  375. Path = NormalizeItemPath(playlistPath, child.Path),
  376. Title = child.Name,
  377. Album = child.Album
  378. };
  379. if (child is IHasAlbumArtist hasAlbumArtist)
  380. {
  381. entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
  382. }
  383. if (child.RunTimeTicks.HasValue)
  384. {
  385. entry.Duration = TimeSpan.FromTicks(child.RunTimeTicks.Value);
  386. }
  387. playlist.PlaylistEntries.Add(entry);
  388. }
  389. string text = new M3uContent().ToText(playlist);
  390. File.WriteAllText(playlistPath, text);
  391. }
  392. if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
  393. {
  394. var playlist = new PlsPlaylist();
  395. foreach (var child in item.GetLinkedChildren())
  396. {
  397. var entry = new PlsPlaylistEntry()
  398. {
  399. Path = NormalizeItemPath(playlistPath, child.Path),
  400. Title = child.Name
  401. };
  402. if (child.RunTimeTicks.HasValue)
  403. {
  404. entry.Length = TimeSpan.FromTicks(child.RunTimeTicks.Value);
  405. }
  406. playlist.PlaylistEntries.Add(entry);
  407. }
  408. string text = new PlsContent().ToText(playlist);
  409. File.WriteAllText(playlistPath, text);
  410. }
  411. }
  412. private string NormalizeItemPath(string playlistPath, string itemPath)
  413. {
  414. return MakeRelativePath(Path.GetDirectoryName(playlistPath), itemPath);
  415. }
  416. private static string MakeRelativePath(string folderPath, string fileAbsolutePath)
  417. {
  418. if (string.IsNullOrEmpty(folderPath))
  419. {
  420. throw new ArgumentException("Folder path was null or empty.", nameof(folderPath));
  421. }
  422. if (string.IsNullOrEmpty(fileAbsolutePath))
  423. {
  424. throw new ArgumentException("File absolute path was null or empty.", nameof(fileAbsolutePath));
  425. }
  426. if (!folderPath.EndsWith(Path.DirectorySeparatorChar))
  427. {
  428. folderPath += Path.DirectorySeparatorChar;
  429. }
  430. var folderUri = new Uri(folderPath);
  431. var fileAbsoluteUri = new Uri(fileAbsolutePath);
  432. // path can't be made relative
  433. if (folderUri.Scheme != fileAbsoluteUri.Scheme)
  434. {
  435. return fileAbsolutePath;
  436. }
  437. var relativeUri = folderUri.MakeRelativeUri(fileAbsoluteUri);
  438. string relativePath = Uri.UnescapeDataString(relativeUri.ToString());
  439. if (fileAbsoluteUri.Scheme.Equals("file", StringComparison.OrdinalIgnoreCase))
  440. {
  441. relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
  442. }
  443. return relativePath;
  444. }
  445. public Folder GetPlaylistsFolder(Guid userId)
  446. {
  447. const string TypeName = "PlaylistsFolder";
  448. return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal)) ??
  449. _libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal));
  450. }
  451. }
  452. }