GroupController.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.Linq;
  5. using System.Threading;
  6. using System.Threading.Tasks;
  7. using Jellyfin.Data.Entities;
  8. using Jellyfin.Data.Enums;
  9. using MediaBrowser.Controller.Entities;
  10. using MediaBrowser.Controller.Library;
  11. using MediaBrowser.Controller.Session;
  12. using MediaBrowser.Controller.SyncPlay;
  13. using MediaBrowser.Model.SyncPlay;
  14. using Microsoft.Extensions.Logging;
  15. namespace Emby.Server.Implementations.SyncPlay
  16. {
  17. /// <summary>
  18. /// Class GroupController.
  19. /// </summary>
  20. /// <remarks>
  21. /// Class is not thread-safe, external locking is required when accessing methods.
  22. /// </remarks>
  23. public class GroupController : IGroupController, IGroupStateContext
  24. {
  25. /// <summary>
  26. /// The logger.
  27. /// </summary>
  28. private readonly ILogger _logger;
  29. /// <summary>
  30. /// The user manager.
  31. /// </summary>
  32. private readonly IUserManager _userManager;
  33. /// <summary>
  34. /// The session manager.
  35. /// </summary>
  36. private readonly ISessionManager _sessionManager;
  37. /// <summary>
  38. /// The library manager.
  39. /// </summary>
  40. private readonly ILibraryManager _libraryManager;
  41. /// <summary>
  42. /// The SyncPlay manager.
  43. /// </summary>
  44. private readonly ISyncPlayManager _syncPlayManager;
  45. /// <summary>
  46. /// Internal group state.
  47. /// </summary>
  48. /// <value>The group's state.</value>
  49. private IGroupState _state;
  50. /// <summary>
  51. /// Initializes a new instance of the <see cref="GroupController" /> class.
  52. /// </summary>
  53. /// <param name="logger">The logger.</param>
  54. /// <param name="userManager">The user manager.</param>
  55. /// <param name="sessionManager">The session manager.</param>
  56. /// <param name="libraryManager">The library manager.</param>
  57. /// <param name="syncPlayManager">The SyncPlay manager.</param>
  58. public GroupController(
  59. ILogger logger,
  60. IUserManager userManager,
  61. ISessionManager sessionManager,
  62. ILibraryManager libraryManager,
  63. ISyncPlayManager syncPlayManager)
  64. {
  65. _logger = logger;
  66. _userManager = userManager;
  67. _sessionManager = sessionManager;
  68. _libraryManager = libraryManager;
  69. _syncPlayManager = syncPlayManager;
  70. _state = new IdleGroupState(_logger);
  71. }
  72. /// <summary>
  73. /// Gets the default ping value used for sessions.
  74. /// </summary>
  75. public long DefaultPing { get; } = 500;
  76. /// <summary>
  77. /// Gets the maximum time offset error accepted for dates reported by clients, in milliseconds.
  78. /// </summary>
  79. public long TimeSyncOffset { get; } = 2000;
  80. /// <summary>
  81. /// Gets the maximum offset error accepted for position reported by clients, in milliseconds.
  82. /// </summary>
  83. public long MaxPlaybackOffset { get; } = 500;
  84. /// <summary>
  85. /// Gets the group identifier.
  86. /// </summary>
  87. /// <value>The group identifier.</value>
  88. public Guid GroupId { get; } = Guid.NewGuid();
  89. /// <summary>
  90. /// Gets the group name.
  91. /// </summary>
  92. /// <value>The group name.</value>
  93. public string GroupName { get; private set; }
  94. /// <summary>
  95. /// Gets the group identifier.
  96. /// </summary>
  97. /// <value>The group identifier.</value>
  98. public PlayQueueManager PlayQueue { get; } = new PlayQueueManager();
  99. /// <summary>
  100. /// Gets the runtime ticks of current playing item.
  101. /// </summary>
  102. /// <value>The runtime ticks of current playing item.</value>
  103. public long RunTimeTicks { get; private set; }
  104. /// <summary>
  105. /// Gets or sets the position ticks.
  106. /// </summary>
  107. /// <value>The position ticks.</value>
  108. public long PositionTicks { get; set; }
  109. /// <summary>
  110. /// Gets or sets the last activity.
  111. /// </summary>
  112. /// <value>The last activity.</value>
  113. public DateTime LastActivity { get; set; }
  114. /// <summary>
  115. /// Gets the participants.
  116. /// </summary>
  117. /// <value>The participants, or members of the group.</value>
  118. public Dictionary<string, GroupMember> Participants { get; } =
  119. new Dictionary<string, GroupMember>(StringComparer.OrdinalIgnoreCase);
  120. /// <summary>
  121. /// Adds the session to the group.
  122. /// </summary>
  123. /// <param name="session">The session.</param>
  124. private void AddSession(SessionInfo session)
  125. {
  126. Participants.TryAdd(
  127. session.Id,
  128. new GroupMember
  129. {
  130. Session = session,
  131. Ping = DefaultPing,
  132. IsBuffering = false
  133. });
  134. }
  135. /// <summary>
  136. /// Removes the session from the group.
  137. /// </summary>
  138. /// <param name="session">The session.</param>
  139. private void RemoveSession(SessionInfo session)
  140. {
  141. Participants.Remove(session.Id);
  142. }
  143. /// <summary>
  144. /// Filters sessions of this group.
  145. /// </summary>
  146. /// <param name="from">The current session.</param>
  147. /// <param name="type">The filtering type.</param>
  148. /// <returns>The array of sessions matching the filter.</returns>
  149. private IEnumerable<SessionInfo> FilterSessions(SessionInfo from, SyncPlayBroadcastType type)
  150. {
  151. return type switch
  152. {
  153. SyncPlayBroadcastType.CurrentSession => new SessionInfo[] { from },
  154. SyncPlayBroadcastType.AllGroup => Participants
  155. .Values
  156. .Select(session => session.Session),
  157. SyncPlayBroadcastType.AllExceptCurrentSession => Participants
  158. .Values
  159. .Select(session => session.Session)
  160. .Where(session => !session.Id.Equals(from.Id, StringComparison.OrdinalIgnoreCase)),
  161. SyncPlayBroadcastType.AllReady => Participants
  162. .Values
  163. .Where(session => !session.IsBuffering)
  164. .Select(session => session.Session),
  165. _ => Enumerable.Empty<SessionInfo>()
  166. };
  167. }
  168. /// <summary>
  169. /// Checks if a given user can access a given item, that is, the user has access to a folder where the item is stored.
  170. /// </summary>
  171. /// <param name="user">The user.</param>
  172. /// <param name="item">The item.</param>
  173. /// <returns><c>true</c> if the user can access the item, <c>false</c> otherwise.</returns>
  174. private bool HasAccessToItem(User user, BaseItem item)
  175. {
  176. var collections = _libraryManager.GetCollectionFolders(item)
  177. .Select(folder => folder.Id.ToString("N", CultureInfo.InvariantCulture));
  178. return collections.Intersect(user.GetPreference(PreferenceKind.EnabledFolders)).Any();
  179. }
  180. /// <summary>
  181. /// Checks if a given user can access all items of a given queue, that is,
  182. /// the user has the required minimum parental access and has access to all required folders.
  183. /// </summary>
  184. /// <param name="user">The user.</param>
  185. /// <param name="queue">The queue.</param>
  186. /// <returns><c>true</c> if the user can access all the items in the queue, <c>false</c> otherwise.</returns>
  187. private bool HasAccessToQueue(User user, IEnumerable<Guid> queue)
  188. {
  189. // Check if queue is empty.
  190. if (!queue?.Any() ?? true)
  191. {
  192. return true;
  193. }
  194. foreach (var itemId in queue)
  195. {
  196. var item = _libraryManager.GetItemById(itemId);
  197. if (user.MaxParentalAgeRating.HasValue && item.InheritedParentalRatingValue > user.MaxParentalAgeRating)
  198. {
  199. return false;
  200. }
  201. if (!user.HasPermission(PermissionKind.EnableAllFolders) && !HasAccessToItem(user, item))
  202. {
  203. return false;
  204. }
  205. }
  206. return true;
  207. }
  208. private bool AllUsersHaveAccessToQueue(IEnumerable<Guid> queue)
  209. {
  210. // Check if queue is empty.
  211. if (!queue?.Any() ?? true)
  212. {
  213. return true;
  214. }
  215. // Get list of users.
  216. var users = Participants
  217. .Values
  218. .Select(participant => _userManager.GetUserById(participant.Session.UserId));
  219. // Find problematic users.
  220. var usersWithNoAccess = users.Where(user => !HasAccessToQueue(user, queue));
  221. // All users must be able to access the queue.
  222. return !usersWithNoAccess.Any();
  223. }
  224. /// <inheritdoc />
  225. public bool IsGroupEmpty() => Participants.Count == 0;
  226. /// <inheritdoc />
  227. public void CreateGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken)
  228. {
  229. GroupName = request.GroupName;
  230. AddSession(session);
  231. _syncPlayManager.AddSessionToGroup(session, this);
  232. var sessionIsPlayingAnItem = session.FullNowPlayingItem != null;
  233. RestartCurrentItem();
  234. if (sessionIsPlayingAnItem)
  235. {
  236. var playlist = session.NowPlayingQueue.Select(item => item.Id);
  237. PlayQueue.Reset();
  238. PlayQueue.SetPlaylist(playlist);
  239. PlayQueue.SetPlayingItemById(session.FullNowPlayingItem.Id);
  240. RunTimeTicks = session.FullNowPlayingItem.RunTimeTicks ?? 0;
  241. PositionTicks = session.PlayState.PositionTicks ?? 0;
  242. // Mantain playstate.
  243. var waitingState = new WaitingGroupState(_logger)
  244. {
  245. ResumePlaying = !session.PlayState.IsPaused
  246. };
  247. SetState(waitingState);
  248. }
  249. var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, GetInfo());
  250. SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken);
  251. _state.SessionJoined(this, _state.Type, session, cancellationToken);
  252. _logger.LogInformation("InitGroup: {0} created group {1}.", session.Id, GroupId.ToString());
  253. }
  254. /// <inheritdoc />
  255. public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken)
  256. {
  257. AddSession(session);
  258. _syncPlayManager.AddSessionToGroup(session, this);
  259. var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, GetInfo());
  260. SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken);
  261. var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName);
  262. SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
  263. _state.SessionJoined(this, _state.Type, session, cancellationToken);
  264. _logger.LogInformation("SessionJoin: {0} joined group {1}.", session.Id, GroupId.ToString());
  265. }
  266. /// <inheritdoc />
  267. public void SessionRestore(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken)
  268. {
  269. var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, GetInfo());
  270. SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken);
  271. var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName);
  272. SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
  273. _state.SessionJoined(this, _state.Type, session, cancellationToken);
  274. _logger.LogInformation("SessionRestore: {0} re-joined group {1}.", session.Id, GroupId.ToString());
  275. }
  276. /// <inheritdoc />
  277. public void SessionLeave(SessionInfo session, CancellationToken cancellationToken)
  278. {
  279. _state.SessionLeaving(this, _state.Type, session, cancellationToken);
  280. RemoveSession(session);
  281. _syncPlayManager.RemoveSessionFromGroup(session, this);
  282. var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupLeft, GroupId.ToString());
  283. SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken);
  284. var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserLeft, session.UserName);
  285. SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
  286. _logger.LogInformation("SessionLeave: {0} left group {1}.", session.Id, GroupId.ToString());
  287. }
  288. /// <inheritdoc />
  289. public void HandleRequest(SessionInfo session, IGroupPlaybackRequest request, CancellationToken cancellationToken)
  290. {
  291. // The server's job is to maintain a consistent state for clients to reference
  292. // and notify clients of state changes. The actual syncing of media playback
  293. // happens client side. Clients are aware of the server's time and use it to sync.
  294. _logger.LogInformation("HandleRequest: {0} requested {1}, group {2} in {3} state.", session.Id, request.Type, GroupId.ToString(), _state.Type);
  295. request.Apply(this, _state, session, cancellationToken);
  296. }
  297. /// <inheritdoc />
  298. public GroupInfoDto GetInfo()
  299. {
  300. return new GroupInfoDto()
  301. {
  302. GroupId = GroupId.ToString(),
  303. GroupName = GroupName,
  304. State = _state.Type,
  305. Participants = Participants.Values.Select(session => session.Session.UserName).Distinct().ToList(),
  306. LastUpdatedAt = DateToUTCString(DateTime.UtcNow)
  307. };
  308. }
  309. /// <inheritdoc />
  310. public bool HasAccessToPlayQueue(User user)
  311. {
  312. var items = PlayQueue.GetPlaylist().Select(item => item.ItemId);
  313. return HasAccessToQueue(user, items);
  314. }
  315. /// <inheritdoc />
  316. public void SetIgnoreGroupWait(SessionInfo session, bool ignoreGroupWait)
  317. {
  318. if (!Participants.ContainsKey(session.Id))
  319. {
  320. return;
  321. }
  322. Participants[session.Id].IgnoreGroupWait = ignoreGroupWait;
  323. }
  324. /// <inheritdoc />
  325. public void SetState(IGroupState state)
  326. {
  327. _logger.LogInformation("SetState: {0} switching from {1} to {2}.", GroupId.ToString(), _state.Type, state.Type);
  328. this._state = state;
  329. }
  330. /// <inheritdoc />
  331. public Task SendGroupUpdate<T>(SessionInfo from, SyncPlayBroadcastType type, GroupUpdate<T> message, CancellationToken cancellationToken)
  332. {
  333. IEnumerable<Task> GetTasks()
  334. {
  335. foreach (var session in FilterSessions(from, type))
  336. {
  337. yield return _sessionManager.SendSyncPlayGroupUpdate(session, message, cancellationToken);
  338. }
  339. }
  340. return Task.WhenAll(GetTasks());
  341. }
  342. /// <inheritdoc />
  343. public Task SendCommand(SessionInfo from, SyncPlayBroadcastType type, SendCommand message, CancellationToken cancellationToken)
  344. {
  345. IEnumerable<Task> GetTasks()
  346. {
  347. foreach (var session in FilterSessions(from, type))
  348. {
  349. yield return _sessionManager.SendSyncPlayCommand(session, message, cancellationToken);
  350. }
  351. }
  352. return Task.WhenAll(GetTasks());
  353. }
  354. /// <inheritdoc />
  355. public SendCommand NewSyncPlayCommand(SendCommandType type)
  356. {
  357. return new SendCommand()
  358. {
  359. GroupId = GroupId.ToString(),
  360. PlaylistItemId = PlayQueue.GetPlayingItemPlaylistId(),
  361. PositionTicks = PositionTicks,
  362. Command = type,
  363. When = DateToUTCString(LastActivity),
  364. EmittedAt = DateToUTCString(DateTime.UtcNow)
  365. };
  366. }
  367. /// <inheritdoc />
  368. public GroupUpdate<T> NewSyncPlayGroupUpdate<T>(GroupUpdateType type, T data)
  369. {
  370. return new GroupUpdate<T>()
  371. {
  372. GroupId = GroupId.ToString(),
  373. Type = type,
  374. Data = data
  375. };
  376. }
  377. /// <inheritdoc />
  378. public string DateToUTCString(DateTime dateTime)
  379. {
  380. return dateTime.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture);
  381. }
  382. /// <inheritdoc />
  383. public long SanitizePositionTicks(long? positionTicks)
  384. {
  385. var ticks = positionTicks ?? 0;
  386. return Math.Clamp(ticks, 0, RunTimeTicks);
  387. }
  388. /// <inheritdoc />
  389. public void UpdatePing(SessionInfo session, long ping)
  390. {
  391. if (Participants.TryGetValue(session.Id, out GroupMember value))
  392. {
  393. value.Ping = ping;
  394. }
  395. }
  396. /// <inheritdoc />
  397. public long GetHighestPing()
  398. {
  399. long max = long.MinValue;
  400. foreach (var session in Participants.Values)
  401. {
  402. max = Math.Max(max, session.Ping);
  403. }
  404. return max;
  405. }
  406. /// <inheritdoc />
  407. public void SetBuffering(SessionInfo session, bool isBuffering)
  408. {
  409. if (Participants.TryGetValue(session.Id, out GroupMember value))
  410. {
  411. value.IsBuffering = isBuffering;
  412. }
  413. }
  414. /// <inheritdoc />
  415. public void SetAllBuffering(bool isBuffering)
  416. {
  417. foreach (var session in Participants.Values)
  418. {
  419. session.IsBuffering = isBuffering;
  420. }
  421. }
  422. /// <inheritdoc />
  423. public bool IsBuffering()
  424. {
  425. foreach (var session in Participants.Values)
  426. {
  427. if (session.IsBuffering && !session.IgnoreGroupWait)
  428. {
  429. return true;
  430. }
  431. }
  432. return false;
  433. }
  434. /// <inheritdoc />
  435. public bool SetPlayQueue(IEnumerable<Guid> playQueue, int playingItemPosition, long startPositionTicks)
  436. {
  437. // Ignore on empty queue or invalid item position.
  438. if (!playQueue.Any() || playingItemPosition >= playQueue.Count() || playingItemPosition < 0)
  439. {
  440. return false;
  441. }
  442. // Check if participants can access the new playing queue.
  443. if (!AllUsersHaveAccessToQueue(playQueue))
  444. {
  445. return false;
  446. }
  447. PlayQueue.Reset();
  448. PlayQueue.SetPlaylist(playQueue);
  449. PlayQueue.SetPlayingItemByIndex(playingItemPosition);
  450. var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId());
  451. RunTimeTicks = item.RunTimeTicks ?? 0;
  452. PositionTicks = startPositionTicks;
  453. LastActivity = DateTime.UtcNow;
  454. return true;
  455. }
  456. /// <inheritdoc />
  457. public bool SetPlayingItem(string playlistItemId)
  458. {
  459. var itemFound = PlayQueue.SetPlayingItemByPlaylistId(playlistItemId);
  460. if (itemFound)
  461. {
  462. var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId());
  463. RunTimeTicks = item.RunTimeTicks ?? 0;
  464. }
  465. else
  466. {
  467. RunTimeTicks = 0;
  468. }
  469. RestartCurrentItem();
  470. return itemFound;
  471. }
  472. /// <inheritdoc />
  473. public bool RemoveFromPlayQueue(IEnumerable<string> playlistItemIds)
  474. {
  475. var playingItemRemoved = PlayQueue.RemoveFromPlaylist(playlistItemIds);
  476. if (playingItemRemoved)
  477. {
  478. var itemId = PlayQueue.GetPlayingItemId();
  479. if (!itemId.Equals(Guid.Empty))
  480. {
  481. var item = _libraryManager.GetItemById(itemId);
  482. RunTimeTicks = item.RunTimeTicks ?? 0;
  483. }
  484. else
  485. {
  486. RunTimeTicks = 0;
  487. }
  488. RestartCurrentItem();
  489. }
  490. return playingItemRemoved;
  491. }
  492. /// <inheritdoc />
  493. public bool MoveItemInPlayQueue(string playlistItemId, int newIndex)
  494. {
  495. return PlayQueue.MovePlaylistItem(playlistItemId, newIndex);
  496. }
  497. /// <inheritdoc />
  498. public bool AddToPlayQueue(IEnumerable<Guid> newItems, string mode)
  499. {
  500. // Ignore on empty list.
  501. if (!newItems.Any())
  502. {
  503. return false;
  504. }
  505. // Check if participants can access the new playing queue.
  506. if (!AllUsersHaveAccessToQueue(newItems))
  507. {
  508. return false;
  509. }
  510. if (mode.Equals("next", StringComparison.OrdinalIgnoreCase))
  511. {
  512. PlayQueue.QueueNext(newItems);
  513. }
  514. else
  515. {
  516. PlayQueue.Queue(newItems);
  517. }
  518. return true;
  519. }
  520. /// <inheritdoc />
  521. public void RestartCurrentItem()
  522. {
  523. PositionTicks = 0;
  524. LastActivity = DateTime.UtcNow;
  525. }
  526. /// <inheritdoc />
  527. public bool NextItemInQueue()
  528. {
  529. var update = PlayQueue.Next();
  530. if (update)
  531. {
  532. var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId());
  533. RunTimeTicks = item.RunTimeTicks ?? 0;
  534. RestartCurrentItem();
  535. return true;
  536. }
  537. else
  538. {
  539. return false;
  540. }
  541. }
  542. /// <inheritdoc />
  543. public bool PreviousItemInQueue()
  544. {
  545. var update = PlayQueue.Previous();
  546. if (update)
  547. {
  548. var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId());
  549. RunTimeTicks = item.RunTimeTicks ?? 0;
  550. RestartCurrentItem();
  551. return true;
  552. }
  553. else
  554. {
  555. return false;
  556. }
  557. }
  558. /// <inheritdoc />
  559. public void SetRepeatMode(string mode)
  560. {
  561. switch (mode)
  562. {
  563. case "RepeatOne":
  564. PlayQueue.SetRepeatMode(GroupRepeatMode.RepeatOne);
  565. break;
  566. case "RepeatAll":
  567. PlayQueue.SetRepeatMode(GroupRepeatMode.RepeatAll);
  568. break;
  569. default:
  570. // On unknown values, default to repeat none.
  571. PlayQueue.SetRepeatMode(GroupRepeatMode.RepeatNone);
  572. break;
  573. }
  574. }
  575. /// <inheritdoc />
  576. public void SetShuffleMode(string mode)
  577. {
  578. switch (mode)
  579. {
  580. case "Shuffle":
  581. PlayQueue.SetShuffleMode(GroupShuffleMode.Shuffle);
  582. break;
  583. default:
  584. // On unknown values, default to sorted playlist.
  585. PlayQueue.SetShuffleMode(GroupShuffleMode.Sorted);
  586. break;
  587. }
  588. }
  589. /// <inheritdoc />
  590. public PlayQueueUpdate GetPlayQueueUpdate(PlayQueueUpdateReason reason)
  591. {
  592. var startPositionTicks = PositionTicks;
  593. if (_state.Type.Equals(GroupStateType.Playing))
  594. {
  595. var currentTime = DateTime.UtcNow;
  596. var elapsedTime = currentTime - LastActivity;
  597. // Elapsed time is negative if event happens
  598. // during the delay added to account for latency.
  599. // In this phase clients haven't started the playback yet.
  600. // In other words, LastActivity is in the future,
  601. // when playback unpause is supposed to happen.
  602. // Adjust ticks only if playback actually started.
  603. startPositionTicks += Math.Max(elapsedTime.Ticks, 0);
  604. }
  605. return new PlayQueueUpdate()
  606. {
  607. Reason = reason,
  608. LastUpdate = DateToUTCString(PlayQueue.LastChange),
  609. Playlist = PlayQueue.GetPlaylist(),
  610. PlayingItemIndex = PlayQueue.PlayingItemIndex,
  611. StartPositionTicks = startPositionTicks,
  612. ShuffleMode = PlayQueue.ShuffleMode,
  613. RepeatMode = PlayQueue.RepeatMode
  614. };
  615. }
  616. }
  617. }