SyncPlayManager.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Threading;
  5. using Jellyfin.Data.Enums;
  6. using MediaBrowser.Controller.Library;
  7. using MediaBrowser.Controller.Session;
  8. using MediaBrowser.Controller.SyncPlay;
  9. using MediaBrowser.Model.SyncPlay;
  10. using Microsoft.Extensions.Logging;
  11. namespace Emby.Server.Implementations.SyncPlay
  12. {
  13. /// <summary>
  14. /// Class SyncPlayManager.
  15. /// </summary>
  16. public class SyncPlayManager : ISyncPlayManager, IDisposable
  17. {
  18. /// <summary>
  19. /// The logger.
  20. /// </summary>
  21. private readonly ILogger<SyncPlayManager> _logger;
  22. /// <summary>
  23. /// The user manager.
  24. /// </summary>
  25. private readonly IUserManager _userManager;
  26. /// <summary>
  27. /// The session manager.
  28. /// </summary>
  29. private readonly ISessionManager _sessionManager;
  30. /// <summary>
  31. /// The library manager.
  32. /// </summary>
  33. private readonly ILibraryManager _libraryManager;
  34. /// <summary>
  35. /// The map between sessions and groups.
  36. /// </summary>
  37. private readonly Dictionary<string, IGroupController> _sessionToGroupMap =
  38. new Dictionary<string, IGroupController>(StringComparer.OrdinalIgnoreCase);
  39. /// <summary>
  40. /// The groups.
  41. /// </summary>
  42. private readonly Dictionary<Guid, IGroupController> _groups =
  43. new Dictionary<Guid, IGroupController>();
  44. /// <summary>
  45. /// Lock used for accesing any group.
  46. /// </summary>
  47. private readonly object _groupsLock = new object();
  48. private bool _disposed = false;
  49. /// <summary>
  50. /// Initializes a new instance of the <see cref="SyncPlayManager" /> class.
  51. /// </summary>
  52. /// <param name="logger">The logger.</param>
  53. /// <param name="userManager">The user manager.</param>
  54. /// <param name="sessionManager">The session manager.</param>
  55. /// <param name="libraryManager">The library manager.</param>
  56. public SyncPlayManager(
  57. ILogger<SyncPlayManager> logger,
  58. IUserManager userManager,
  59. ISessionManager sessionManager,
  60. ILibraryManager libraryManager)
  61. {
  62. _logger = logger;
  63. _userManager = userManager;
  64. _sessionManager = sessionManager;
  65. _libraryManager = libraryManager;
  66. _sessionManager.SessionStarted += OnSessionManagerSessionStarted;
  67. }
  68. /// <inheritdoc />
  69. public void Dispose()
  70. {
  71. Dispose(true);
  72. GC.SuppressFinalize(this);
  73. }
  74. /// <inheritdoc />
  75. public void NewGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken)
  76. {
  77. // TODO: create abstract class for GroupRequests to avoid explicit request type here.
  78. if (!IsRequestValid(session, GroupRequestType.NewGroup, request))
  79. {
  80. return;
  81. }
  82. lock (_groupsLock)
  83. {
  84. if (IsSessionInGroup(session))
  85. {
  86. LeaveGroup(session, cancellationToken);
  87. }
  88. var group = new GroupController(_logger, _userManager, _sessionManager, _libraryManager);
  89. _groups[group.GroupId] = group;
  90. AddSessionToGroup(session, group);
  91. group.CreateGroup(session, request, cancellationToken);
  92. }
  93. }
  94. /// <inheritdoc />
  95. public void JoinGroup(SessionInfo session, Guid groupId, JoinGroupRequest request, CancellationToken cancellationToken)
  96. {
  97. // TODO: create abstract class for GroupRequests to avoid explicit request type here.
  98. if (!IsRequestValid(session, GroupRequestType.JoinGroup, request))
  99. {
  100. return;
  101. }
  102. var user = _userManager.GetUserById(session.UserId);
  103. lock (_groupsLock)
  104. {
  105. _groups.TryGetValue(groupId, out IGroupController group);
  106. if (group == null)
  107. {
  108. _logger.LogWarning("JoinGroup: {SessionId} tried to join group {GroupId} that does not exist.", session.Id, groupId);
  109. var error = new GroupUpdate<string>(Guid.Empty, GroupUpdateType.GroupDoesNotExist, string.Empty);
  110. _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None);
  111. return;
  112. }
  113. if (!group.HasAccessToPlayQueue(user))
  114. {
  115. _logger.LogWarning("JoinGroup: {SessionId} does not have access to some content from the playing queue of group {GroupId}.", session.Id, group.GroupId.ToString());
  116. var error = new GroupUpdate<string>(group.GroupId, GroupUpdateType.LibraryAccessDenied, string.Empty);
  117. _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None);
  118. return;
  119. }
  120. if (IsSessionInGroup(session))
  121. {
  122. if (GetSessionGroup(session).Equals(groupId))
  123. {
  124. group.SessionRestore(session, request, cancellationToken);
  125. return;
  126. }
  127. LeaveGroup(session, cancellationToken);
  128. }
  129. AddSessionToGroup(session, group);
  130. group.SessionJoin(session, request, cancellationToken);
  131. }
  132. }
  133. /// <inheritdoc />
  134. public void LeaveGroup(SessionInfo session, CancellationToken cancellationToken)
  135. {
  136. // TODO: create abstract class for GroupRequests to avoid explicit request type here.
  137. if (!IsRequestValid(session, GroupRequestType.LeaveGroup))
  138. {
  139. return;
  140. }
  141. // TODO: determine what happens to users that are in a group and get their permissions revoked.
  142. lock (_groupsLock)
  143. {
  144. _sessionToGroupMap.TryGetValue(session.Id, out var group);
  145. if (group == null)
  146. {
  147. _logger.LogWarning("LeaveGroup: {SessionId} does not belong to any group.", session.Id);
  148. var error = new GroupUpdate<string>(Guid.Empty, GroupUpdateType.NotInGroup, string.Empty);
  149. _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None);
  150. return;
  151. }
  152. RemoveSessionFromGroup(session, group);
  153. group.SessionLeave(session, cancellationToken);
  154. if (group.IsGroupEmpty())
  155. {
  156. _logger.LogInformation("LeaveGroup: removing empty group {GroupId}.", group.GroupId);
  157. _groups.Remove(group.GroupId, out _);
  158. }
  159. }
  160. }
  161. /// <inheritdoc />
  162. public List<GroupInfoDto> ListGroups(SessionInfo session)
  163. {
  164. // TODO: create abstract class for GroupRequests to avoid explicit request type here.
  165. if (!IsRequestValid(session, GroupRequestType.ListGroups))
  166. {
  167. return new List<GroupInfoDto>();
  168. }
  169. var user = _userManager.GetUserById(session.UserId);
  170. lock (_groupsLock)
  171. {
  172. return _groups
  173. .Values
  174. .Where(group => group.HasAccessToPlayQueue(user))
  175. .Select(group => group.GetInfo())
  176. .ToList();
  177. }
  178. }
  179. /// <inheritdoc />
  180. public void HandleRequest(SessionInfo session, IGroupPlaybackRequest request, CancellationToken cancellationToken)
  181. {
  182. // TODO: create abstract class for GroupRequests to avoid explicit request type here.
  183. if (!IsRequestValid(session, GroupRequestType.Playback, request))
  184. {
  185. return;
  186. }
  187. lock (_groupsLock)
  188. {
  189. _sessionToGroupMap.TryGetValue(session.Id, out var group);
  190. if (group == null)
  191. {
  192. _logger.LogWarning("HandleRequest: {SessionId} does not belong to any group.", session.Id);
  193. var error = new GroupUpdate<string>(Guid.Empty, GroupUpdateType.NotInGroup, string.Empty);
  194. _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None);
  195. return;
  196. }
  197. group.HandleRequest(session, request, cancellationToken);
  198. }
  199. }
  200. /// <summary>
  201. /// Releases unmanaged and optionally managed resources.
  202. /// </summary>
  203. /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
  204. protected virtual void Dispose(bool disposing)
  205. {
  206. if (_disposed)
  207. {
  208. return;
  209. }
  210. _sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
  211. _disposed = true;
  212. }
  213. private void OnSessionManagerSessionStarted(object sender, SessionEventArgs e)
  214. {
  215. var session = e.SessionInfo;
  216. lock (_groupsLock)
  217. {
  218. if (!IsSessionInGroup(session))
  219. {
  220. return;
  221. }
  222. var groupId = GetSessionGroup(session);
  223. var request = new JoinGroupRequest(groupId);
  224. JoinGroup(session, groupId, request, CancellationToken.None);
  225. }
  226. }
  227. /// <summary>
  228. /// Checks if a given session has joined a group.
  229. /// </summary>
  230. /// <remarks>
  231. /// Not thread-safe, call only under groups-lock.
  232. /// </remarks>
  233. /// <param name="session">The session.</param>
  234. /// <returns><c>true</c> if the session has joined a group, <c>false</c> otherwise.</returns>
  235. private bool IsSessionInGroup(SessionInfo session)
  236. {
  237. return _sessionToGroupMap.ContainsKey(session.Id);
  238. }
  239. /// <summary>
  240. /// Gets the group joined by the given session, if any.
  241. /// </summary>
  242. /// <remarks>
  243. /// Not thread-safe, call only under groups-lock.
  244. /// </remarks>
  245. /// <param name="session">The session.</param>
  246. /// <returns>The group identifier if the session has joined a group, an empty identifier otherwise.</returns>
  247. private Guid GetSessionGroup(SessionInfo session)
  248. {
  249. _sessionToGroupMap.TryGetValue(session.Id, out var group);
  250. return group?.GroupId ?? Guid.Empty;
  251. }
  252. /// <summary>
  253. /// Maps a session to a group.
  254. /// </summary>
  255. /// <remarks>
  256. /// Not thread-safe, call only under groups-lock.
  257. /// </remarks>
  258. /// <param name="session">The session.</param>
  259. /// <param name="group">The group.</param>
  260. /// <exception cref="InvalidOperationException">Thrown when the user is in another group already.</exception>
  261. private void AddSessionToGroup(SessionInfo session, IGroupController group)
  262. {
  263. if (session == null)
  264. {
  265. throw new InvalidOperationException("Session is null!");
  266. }
  267. if (IsSessionInGroup(session))
  268. {
  269. throw new InvalidOperationException("Session in other group already!");
  270. }
  271. _sessionToGroupMap[session.Id] = group ?? throw new InvalidOperationException("Group is null!");
  272. }
  273. /// <summary>
  274. /// Unmaps a session from a group.
  275. /// </summary>
  276. /// <remarks>
  277. /// Not thread-safe, call only under groups-lock.
  278. /// </remarks>
  279. /// <param name="session">The session.</param>
  280. /// <param name="group">The group.</param>
  281. /// <exception cref="InvalidOperationException">Thrown when the user is not found in the specified group.</exception>
  282. private void RemoveSessionFromGroup(SessionInfo session, IGroupController group)
  283. {
  284. if (session == null)
  285. {
  286. throw new InvalidOperationException("Session is null!");
  287. }
  288. if (group == null)
  289. {
  290. throw new InvalidOperationException("Group is null!");
  291. }
  292. if (!IsSessionInGroup(session))
  293. {
  294. throw new InvalidOperationException("Session not in any group!");
  295. }
  296. _sessionToGroupMap.Remove(session.Id, out var tempGroup);
  297. if (!tempGroup.GroupId.Equals(group.GroupId))
  298. {
  299. throw new InvalidOperationException("Session was in wrong group!");
  300. }
  301. }
  302. /// <summary>
  303. /// Checks if a given session is allowed to make a given request.
  304. /// </summary>
  305. /// <param name="session">The session.</param>
  306. /// <param name="requestType">The request type.</param>
  307. /// <param name="request">The request.</param>
  308. /// <param name="checkRequest">Whether to check if request is null.</param>
  309. /// <returns><c>true</c> if the request is valid, <c>false</c> otherwise. Will return <c>false</c> also when session is null.</returns>
  310. private bool IsRequestValid<T>(SessionInfo session, GroupRequestType requestType, T request, bool checkRequest = true)
  311. {
  312. if (session == null || (request == null && checkRequest))
  313. {
  314. return false;
  315. }
  316. var user = _userManager.GetUserById(session.UserId);
  317. if (user.SyncPlayAccess == SyncPlayAccess.None)
  318. {
  319. _logger.LogWarning("IsRequestValid: {SessionId} does not have access to SyncPlay. Requested {RequestType}.", session.Id, requestType);
  320. // TODO: rename to a more generic error. Next PR will fix this.
  321. var error = new GroupUpdate<string>(Guid.Empty, GroupUpdateType.JoinGroupDenied, string.Empty);
  322. _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None);
  323. return false;
  324. }
  325. if (requestType.Equals(GroupRequestType.NewGroup) && user.SyncPlayAccess != SyncPlayAccess.CreateAndJoinGroups)
  326. {
  327. _logger.LogWarning("IsRequestValid: {SessionId} does not have permission to create groups.", session.Id);
  328. var error = new GroupUpdate<string>(Guid.Empty, GroupUpdateType.CreateGroupDenied, string.Empty);
  329. _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None);
  330. return false;
  331. }
  332. return true;
  333. }
  334. /// <summary>
  335. /// Checks if a given session is allowed to make a given type of request.
  336. /// </summary>
  337. /// <param name="session">The session.</param>
  338. /// <param name="requestType">The request type.</param>
  339. /// <returns><c>true</c> if the request is valid, <c>false</c> otherwise. Will return <c>false</c> also when session is null.</returns>
  340. private bool IsRequestValid(SessionInfo session, GroupRequestType requestType)
  341. {
  342. return IsRequestValid(session, requestType, session, false);
  343. }
  344. }
  345. }