PlayQueueManager.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. #nullable disable
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using Jellyfin.Extensions;
  6. using MediaBrowser.Model.SyncPlay;
  7. namespace MediaBrowser.Controller.SyncPlay.Queue
  8. {
  9. /// <summary>
  10. /// Class PlayQueueManager.
  11. /// </summary>
  12. public class PlayQueueManager
  13. {
  14. /// <summary>
  15. /// Placeholder index for when no item is playing.
  16. /// </summary>
  17. /// <value>The no-playing item index.</value>
  18. private const int NoPlayingItemIndex = -1;
  19. /// <summary>
  20. /// The sorted playlist.
  21. /// </summary>
  22. /// <value>The sorted playlist, or play queue of the group.</value>
  23. private List<QueueItem> _sortedPlaylist = new List<QueueItem>();
  24. /// <summary>
  25. /// The shuffled playlist.
  26. /// </summary>
  27. /// <value>The shuffled playlist, or play queue of the group.</value>
  28. private List<QueueItem> _shuffledPlaylist = new List<QueueItem>();
  29. /// <summary>
  30. /// Initializes a new instance of the <see cref="PlayQueueManager" /> class.
  31. /// </summary>
  32. public PlayQueueManager()
  33. {
  34. Reset();
  35. }
  36. /// <summary>
  37. /// Gets the playing item index.
  38. /// </summary>
  39. /// <value>The playing item index.</value>
  40. public int PlayingItemIndex { get; private set; }
  41. /// <summary>
  42. /// Gets the last time the queue has been changed.
  43. /// </summary>
  44. /// <value>The last time the queue has been changed.</value>
  45. public DateTime LastChange { get; private set; }
  46. /// <summary>
  47. /// Gets the shuffle mode.
  48. /// </summary>
  49. /// <value>The shuffle mode.</value>
  50. public GroupShuffleMode ShuffleMode { get; private set; } = GroupShuffleMode.Sorted;
  51. /// <summary>
  52. /// Gets the repeat mode.
  53. /// </summary>
  54. /// <value>The repeat mode.</value>
  55. public GroupRepeatMode RepeatMode { get; private set; } = GroupRepeatMode.RepeatNone;
  56. /// <summary>
  57. /// Checks if an item is playing.
  58. /// </summary>
  59. /// <returns><c>true</c> if an item is playing; <c>false</c> otherwise.</returns>
  60. public bool IsItemPlaying()
  61. {
  62. return PlayingItemIndex != NoPlayingItemIndex;
  63. }
  64. /// <summary>
  65. /// Gets the current playlist considering the shuffle mode.
  66. /// </summary>
  67. /// <returns>The playlist.</returns>
  68. public IReadOnlyList<QueueItem> GetPlaylist()
  69. {
  70. return GetPlaylistInternal();
  71. }
  72. /// <summary>
  73. /// Sets a new playlist. Playing item is reset.
  74. /// </summary>
  75. /// <param name="items">The new items of the playlist.</param>
  76. public void SetPlaylist(IReadOnlyList<Guid> items)
  77. {
  78. _sortedPlaylist.Clear();
  79. _shuffledPlaylist.Clear();
  80. _sortedPlaylist = CreateQueueItemsFromArray(items);
  81. if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
  82. {
  83. _shuffledPlaylist = new List<QueueItem>(_sortedPlaylist);
  84. _shuffledPlaylist.Shuffle();
  85. }
  86. PlayingItemIndex = NoPlayingItemIndex;
  87. LastChange = DateTime.UtcNow;
  88. }
  89. /// <summary>
  90. /// Appends new items to the playlist. The specified order is maintained.
  91. /// </summary>
  92. /// <param name="items">The items to add to the playlist.</param>
  93. public void Queue(IReadOnlyList<Guid> items)
  94. {
  95. var newItems = CreateQueueItemsFromArray(items);
  96. _sortedPlaylist.AddRange(newItems);
  97. if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
  98. {
  99. _shuffledPlaylist.AddRange(newItems);
  100. }
  101. LastChange = DateTime.UtcNow;
  102. }
  103. /// <summary>
  104. /// Shuffles the playlist. Shuffle mode is changed. The playlist gets re-shuffled if already shuffled.
  105. /// </summary>
  106. public void ShufflePlaylist()
  107. {
  108. if (PlayingItemIndex == NoPlayingItemIndex)
  109. {
  110. _shuffledPlaylist = new List<QueueItem>(_sortedPlaylist);
  111. _shuffledPlaylist.Shuffle();
  112. }
  113. else if (ShuffleMode.Equals(GroupShuffleMode.Sorted))
  114. {
  115. // First time shuffle.
  116. var playingItem = _sortedPlaylist[PlayingItemIndex];
  117. _shuffledPlaylist = new List<QueueItem>(_sortedPlaylist);
  118. _shuffledPlaylist.RemoveAt(PlayingItemIndex);
  119. _shuffledPlaylist.Shuffle();
  120. _shuffledPlaylist.Insert(0, playingItem);
  121. PlayingItemIndex = 0;
  122. }
  123. else
  124. {
  125. // Re-shuffle playlist.
  126. var playingItem = _shuffledPlaylist[PlayingItemIndex];
  127. _shuffledPlaylist.RemoveAt(PlayingItemIndex);
  128. _shuffledPlaylist.Shuffle();
  129. _shuffledPlaylist.Insert(0, playingItem);
  130. PlayingItemIndex = 0;
  131. }
  132. ShuffleMode = GroupShuffleMode.Shuffle;
  133. LastChange = DateTime.UtcNow;
  134. }
  135. /// <summary>
  136. /// Resets the playlist to sorted mode. Shuffle mode is changed.
  137. /// </summary>
  138. public void RestoreSortedPlaylist()
  139. {
  140. if (PlayingItemIndex != NoPlayingItemIndex)
  141. {
  142. var playingItem = _shuffledPlaylist[PlayingItemIndex];
  143. PlayingItemIndex = _sortedPlaylist.IndexOf(playingItem);
  144. }
  145. _shuffledPlaylist.Clear();
  146. ShuffleMode = GroupShuffleMode.Sorted;
  147. LastChange = DateTime.UtcNow;
  148. }
  149. /// <summary>
  150. /// Clears the playlist. Shuffle mode is preserved.
  151. /// </summary>
  152. /// <param name="clearPlayingItem">Whether to remove the playing item as well.</param>
  153. public void ClearPlaylist(bool clearPlayingItem)
  154. {
  155. var playingItem = GetPlayingItem();
  156. _sortedPlaylist.Clear();
  157. _shuffledPlaylist.Clear();
  158. LastChange = DateTime.UtcNow;
  159. if (!clearPlayingItem && playingItem != null)
  160. {
  161. _sortedPlaylist.Add(playingItem);
  162. if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
  163. {
  164. _shuffledPlaylist.Add(playingItem);
  165. }
  166. PlayingItemIndex = 0;
  167. }
  168. else
  169. {
  170. PlayingItemIndex = NoPlayingItemIndex;
  171. }
  172. }
  173. /// <summary>
  174. /// Adds new items to the playlist right after the playing item. The specified order is maintained.
  175. /// </summary>
  176. /// <param name="items">The items to add to the playlist.</param>
  177. public void QueueNext(IReadOnlyList<Guid> items)
  178. {
  179. var newItems = CreateQueueItemsFromArray(items);
  180. if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
  181. {
  182. var playingItem = GetPlayingItem();
  183. var sortedPlayingItemIndex = _sortedPlaylist.IndexOf(playingItem);
  184. // Append items to sorted and shuffled playlist as they are.
  185. _sortedPlaylist.InsertRange(sortedPlayingItemIndex + 1, newItems);
  186. _shuffledPlaylist.InsertRange(PlayingItemIndex + 1, newItems);
  187. }
  188. else
  189. {
  190. _sortedPlaylist.InsertRange(PlayingItemIndex + 1, newItems);
  191. }
  192. LastChange = DateTime.UtcNow;
  193. }
  194. /// <summary>
  195. /// Gets playlist identifier of the playing item, if any.
  196. /// </summary>
  197. /// <returns>The playlist identifier of the playing item.</returns>
  198. public Guid GetPlayingItemPlaylistId()
  199. {
  200. var playingItem = GetPlayingItem();
  201. return playingItem?.PlaylistItemId ?? Guid.Empty;
  202. }
  203. /// <summary>
  204. /// Gets the playing item identifier, if any.
  205. /// </summary>
  206. /// <returns>The playing item identifier.</returns>
  207. public Guid GetPlayingItemId()
  208. {
  209. var playingItem = GetPlayingItem();
  210. return playingItem?.ItemId ?? Guid.Empty;
  211. }
  212. /// <summary>
  213. /// Sets the playing item using its identifier. If not in the playlist, the playing item is reset.
  214. /// </summary>
  215. /// <param name="itemId">The new playing item identifier.</param>
  216. public void SetPlayingItemById(Guid itemId)
  217. {
  218. var playlist = GetPlaylistInternal();
  219. PlayingItemIndex = playlist.FindIndex(item => item.ItemId.Equals(itemId));
  220. LastChange = DateTime.UtcNow;
  221. }
  222. /// <summary>
  223. /// Sets the playing item using its playlist identifier. If not in the playlist, the playing item is reset.
  224. /// </summary>
  225. /// <param name="playlistItemId">The new playing item identifier.</param>
  226. /// <returns><c>true</c> if playing item has been set; <c>false</c> if item is not in the playlist.</returns>
  227. public bool SetPlayingItemByPlaylistId(Guid playlistItemId)
  228. {
  229. var playlist = GetPlaylistInternal();
  230. PlayingItemIndex = playlist.FindIndex(item => item.PlaylistItemId.Equals(playlistItemId));
  231. LastChange = DateTime.UtcNow;
  232. return PlayingItemIndex != NoPlayingItemIndex;
  233. }
  234. /// <summary>
  235. /// Sets the playing item using its position. If not in range, the playing item is reset.
  236. /// </summary>
  237. /// <param name="playlistIndex">The new playing item index.</param>
  238. public void SetPlayingItemByIndex(int playlistIndex)
  239. {
  240. var playlist = GetPlaylistInternal();
  241. if (playlistIndex < 0 || playlistIndex > playlist.Count)
  242. {
  243. PlayingItemIndex = NoPlayingItemIndex;
  244. }
  245. else
  246. {
  247. PlayingItemIndex = playlistIndex;
  248. }
  249. LastChange = DateTime.UtcNow;
  250. }
  251. /// <summary>
  252. /// Removes items from the playlist. If not removed, the playing item is preserved.
  253. /// </summary>
  254. /// <param name="playlistItemIds">The items to remove.</param>
  255. /// <returns><c>true</c> if playing item got removed; <c>false</c> otherwise.</returns>
  256. public bool RemoveFromPlaylist(IReadOnlyList<Guid> playlistItemIds)
  257. {
  258. var playingItem = GetPlayingItem();
  259. _sortedPlaylist.RemoveAll(item => playlistItemIds.Contains(item.PlaylistItemId));
  260. _shuffledPlaylist.RemoveAll(item => playlistItemIds.Contains(item.PlaylistItemId));
  261. LastChange = DateTime.UtcNow;
  262. if (playingItem != null)
  263. {
  264. if (playlistItemIds.Contains(playingItem.PlaylistItemId))
  265. {
  266. // Playing item has been removed, picking previous item.
  267. PlayingItemIndex--;
  268. if (PlayingItemIndex < 0)
  269. {
  270. // Was first element, picking next if available.
  271. // Default to no playing item otherwise.
  272. PlayingItemIndex = _sortedPlaylist.Count > 0 ? 0 : NoPlayingItemIndex;
  273. }
  274. return true;
  275. }
  276. else
  277. {
  278. // Restoring playing item.
  279. SetPlayingItemByPlaylistId(playingItem.PlaylistItemId);
  280. return false;
  281. }
  282. }
  283. else
  284. {
  285. return false;
  286. }
  287. }
  288. /// <summary>
  289. /// Moves an item in the playlist to another position.
  290. /// </summary>
  291. /// <param name="playlistItemId">The item to move.</param>
  292. /// <param name="newIndex">The new position.</param>
  293. /// <returns><c>true</c> if the item has been moved; <c>false</c> otherwise.</returns>
  294. public bool MovePlaylistItem(Guid playlistItemId, int newIndex)
  295. {
  296. var playlist = GetPlaylistInternal();
  297. var playingItem = GetPlayingItem();
  298. var oldIndex = playlist.FindIndex(item => item.PlaylistItemId.Equals(playlistItemId));
  299. if (oldIndex < 0)
  300. {
  301. return false;
  302. }
  303. var queueItem = playlist[oldIndex];
  304. playlist.RemoveAt(oldIndex);
  305. newIndex = Math.Clamp(newIndex, 0, playlist.Count);
  306. playlist.Insert(newIndex, queueItem);
  307. LastChange = DateTime.UtcNow;
  308. PlayingItemIndex = playlist.IndexOf(playingItem);
  309. return true;
  310. }
  311. /// <summary>
  312. /// Resets the playlist to its initial state.
  313. /// </summary>
  314. public void Reset()
  315. {
  316. _sortedPlaylist.Clear();
  317. _shuffledPlaylist.Clear();
  318. PlayingItemIndex = NoPlayingItemIndex;
  319. ShuffleMode = GroupShuffleMode.Sorted;
  320. RepeatMode = GroupRepeatMode.RepeatNone;
  321. LastChange = DateTime.UtcNow;
  322. }
  323. /// <summary>
  324. /// Sets the repeat mode.
  325. /// </summary>
  326. /// <param name="mode">The new mode.</param>
  327. public void SetRepeatMode(GroupRepeatMode mode)
  328. {
  329. RepeatMode = mode;
  330. LastChange = DateTime.UtcNow;
  331. }
  332. /// <summary>
  333. /// Sets the shuffle mode.
  334. /// </summary>
  335. /// <param name="mode">The new mode.</param>
  336. public void SetShuffleMode(GroupShuffleMode mode)
  337. {
  338. if (mode.Equals(GroupShuffleMode.Shuffle))
  339. {
  340. ShufflePlaylist();
  341. }
  342. else
  343. {
  344. RestoreSortedPlaylist();
  345. }
  346. }
  347. /// <summary>
  348. /// Toggles the shuffle mode between sorted and shuffled.
  349. /// </summary>
  350. public void ToggleShuffleMode()
  351. {
  352. if (ShuffleMode.Equals(GroupShuffleMode.Sorted))
  353. {
  354. ShufflePlaylist();
  355. }
  356. else
  357. {
  358. RestoreSortedPlaylist();
  359. }
  360. }
  361. /// <summary>
  362. /// Gets the next item in the playlist considering repeat mode and shuffle mode.
  363. /// </summary>
  364. /// <returns>The next item in the playlist.</returns>
  365. public QueueItem GetNextItemPlaylistId()
  366. {
  367. int newIndex;
  368. var playlist = GetPlaylistInternal();
  369. switch (RepeatMode)
  370. {
  371. case GroupRepeatMode.RepeatOne:
  372. newIndex = PlayingItemIndex;
  373. break;
  374. case GroupRepeatMode.RepeatAll:
  375. newIndex = PlayingItemIndex + 1;
  376. if (newIndex >= playlist.Count)
  377. {
  378. newIndex = 0;
  379. }
  380. break;
  381. default:
  382. newIndex = PlayingItemIndex + 1;
  383. break;
  384. }
  385. if (newIndex < 0 || newIndex >= playlist.Count)
  386. {
  387. return null;
  388. }
  389. return playlist[newIndex];
  390. }
  391. /// <summary>
  392. /// Sets the next item in the queue as playing item.
  393. /// </summary>
  394. /// <returns><c>true</c> if the playing item changed; <c>false</c> otherwise.</returns>
  395. public bool Next()
  396. {
  397. if (RepeatMode.Equals(GroupRepeatMode.RepeatOne))
  398. {
  399. LastChange = DateTime.UtcNow;
  400. return true;
  401. }
  402. PlayingItemIndex++;
  403. if (PlayingItemIndex >= _sortedPlaylist.Count)
  404. {
  405. if (RepeatMode.Equals(GroupRepeatMode.RepeatAll))
  406. {
  407. PlayingItemIndex = 0;
  408. }
  409. else
  410. {
  411. PlayingItemIndex = _sortedPlaylist.Count - 1;
  412. return false;
  413. }
  414. }
  415. LastChange = DateTime.UtcNow;
  416. return true;
  417. }
  418. /// <summary>
  419. /// Sets the previous item in the queue as playing item.
  420. /// </summary>
  421. /// <returns><c>true</c> if the playing item changed; <c>false</c> otherwise.</returns>
  422. public bool Previous()
  423. {
  424. if (RepeatMode.Equals(GroupRepeatMode.RepeatOne))
  425. {
  426. LastChange = DateTime.UtcNow;
  427. return true;
  428. }
  429. PlayingItemIndex--;
  430. if (PlayingItemIndex < 0)
  431. {
  432. if (RepeatMode.Equals(GroupRepeatMode.RepeatAll))
  433. {
  434. PlayingItemIndex = _sortedPlaylist.Count - 1;
  435. }
  436. else
  437. {
  438. PlayingItemIndex = 0;
  439. return false;
  440. }
  441. }
  442. LastChange = DateTime.UtcNow;
  443. return true;
  444. }
  445. /// <summary>
  446. /// Creates a list from the array of items. Each item is given an unique playlist identifier.
  447. /// </summary>
  448. /// <returns>The list of queue items.</returns>
  449. private List<QueueItem> CreateQueueItemsFromArray(IReadOnlyList<Guid> items)
  450. {
  451. var list = new List<QueueItem>();
  452. foreach (var item in items)
  453. {
  454. var queueItem = new QueueItem(item);
  455. list.Add(queueItem);
  456. }
  457. return list;
  458. }
  459. /// <summary>
  460. /// Gets the current playlist considering the shuffle mode.
  461. /// </summary>
  462. /// <returns>The playlist.</returns>
  463. private List<QueueItem> GetPlaylistInternal()
  464. {
  465. if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
  466. {
  467. return _shuffledPlaylist;
  468. }
  469. else
  470. {
  471. return _sortedPlaylist;
  472. }
  473. }
  474. /// <summary>
  475. /// Gets the current playing item, depending on the shuffle mode.
  476. /// </summary>
  477. /// <returns>The playing item.</returns>
  478. private QueueItem GetPlayingItem()
  479. {
  480. if (PlayingItemIndex == NoPlayingItemIndex)
  481. {
  482. return null;
  483. }
  484. else if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
  485. {
  486. return _shuffledPlaylist[PlayingItemIndex];
  487. }
  488. else
  489. {
  490. return _sortedPlaylist[PlayingItemIndex];
  491. }
  492. }
  493. }
  494. }