using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; using MediaBrowser.Controller.SyncPlay; using MediaBrowser.Model.SyncPlay; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.SyncPlay { /// /// Class SyncPlayManager. /// public class SyncPlayManager : ISyncPlayManager, IDisposable { /// /// The logger. /// private readonly ILogger _logger; /// /// The user manager. /// private readonly IUserManager _userManager; /// /// The session manager. /// private readonly ISessionManager _sessionManager; /// /// The library manager. /// private readonly ILibraryManager _libraryManager; /// /// The map between sessions and groups. /// private readonly Dictionary _sessionToGroupMap = new Dictionary(StringComparer.OrdinalIgnoreCase); /// /// The groups. /// private readonly Dictionary _groups = new Dictionary(); /// /// Lock used for accesing any group. /// private readonly object _groupsLock = new object(); private bool _disposed = false; /// /// Initializes a new instance of the class. /// /// The logger. /// The user manager. /// The session manager. /// The library manager. public SyncPlayManager( ILogger logger, IUserManager userManager, ISessionManager sessionManager, ILibraryManager libraryManager) { _logger = logger; _userManager = userManager; _sessionManager = sessionManager; _libraryManager = libraryManager; _sessionManager.SessionEnded += OnSessionManagerSessionEnded; _sessionManager.PlaybackStopped += OnSessionManagerPlaybackStopped; } /// /// Gets all groups. /// /// All groups. public IEnumerable Groups => _groups.Values; /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases unmanaged and optionally managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { if (_disposed) { return; } _sessionManager.SessionEnded -= OnSessionManagerSessionEnded; _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped; _disposed = true; } private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e) { var session = e.SessionInfo; if (!IsSessionInGroup(session)) { return; } LeaveGroup(session, CancellationToken.None); } private void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e) { var session = e.Session; if (!IsSessionInGroup(session)) { return; } LeaveGroup(session, CancellationToken.None); } private bool IsSessionInGroup(SessionInfo session) { return _sessionToGroupMap.ContainsKey(session.Id); } private bool HasAccessToItem(User user, Guid itemId) { var item = _libraryManager.GetItemById(itemId); // Check ParentalRating access var hasParentalRatingAccess = !user.MaxParentalAgeRating.HasValue || item.InheritedParentalRatingValue <= user.MaxParentalAgeRating; if (!user.HasPermission(PermissionKind.EnableAllFolders) && hasParentalRatingAccess) { var collections = _libraryManager.GetCollectionFolders(item).Select( folder => folder.Id.ToString("N", CultureInfo.InvariantCulture)); return collections.Intersect(user.GetPreference(PreferenceKind.EnabledFolders)).Any(); } return hasParentalRatingAccess; } private Guid? GetSessionGroup(SessionInfo session) { _sessionToGroupMap.TryGetValue(session.Id, out var group); return group?.GetGroupId(); } /// public void NewGroup(SessionInfo session, CancellationToken cancellationToken) { var user = _userManager.GetUserById(session.UserId); if (user.SyncPlayAccess != SyncPlayAccess.CreateAndJoinGroups) { _logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id); var error = new GroupUpdate { Type = GroupUpdateType.CreateGroupDenied }; _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); return; } lock (_groupsLock) { if (IsSessionInGroup(session)) { LeaveGroup(session, cancellationToken); } var group = new SyncPlayController(_sessionManager, this); _groups[group.GetGroupId()] = group; group.CreateGroup(session, cancellationToken); } } /// public void JoinGroup(SessionInfo session, Guid groupId, JoinGroupRequest request, CancellationToken cancellationToken) { var user = _userManager.GetUserById(session.UserId); if (user.SyncPlayAccess == SyncPlayAccess.None) { _logger.LogWarning("JoinGroup: {0} does not have access to SyncPlay.", session.Id); var error = new GroupUpdate() { Type = GroupUpdateType.JoinGroupDenied }; _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); return; } lock (_groupsLock) { ISyncPlayController group; _groups.TryGetValue(groupId, out group); if (group == null) { _logger.LogWarning("JoinGroup: {0} tried to join group {0} that does not exist.", session.Id, groupId); var error = new GroupUpdate() { Type = GroupUpdateType.GroupDoesNotExist }; _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); return; } if (!HasAccessToItem(user, group.GetPlayingItemId())) { _logger.LogWarning("JoinGroup: {0} does not have access to {1}.", session.Id, group.GetPlayingItemId()); var error = new GroupUpdate() { GroupId = group.GetGroupId().ToString(), Type = GroupUpdateType.LibraryAccessDenied }; _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); return; } if (IsSessionInGroup(session)) { if (GetSessionGroup(session).Equals(groupId)) { return; } LeaveGroup(session, cancellationToken); } group.SessionJoin(session, request, cancellationToken); } } /// public void LeaveGroup(SessionInfo session, CancellationToken cancellationToken) { // TODO: determine what happens to users that are in a group and get their permissions revoked lock (_groupsLock) { _sessionToGroupMap.TryGetValue(session.Id, out var group); if (group == null) { _logger.LogWarning("LeaveGroup: {0} does not belong to any group.", session.Id); var error = new GroupUpdate() { Type = GroupUpdateType.NotInGroup }; _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); return; } group.SessionLeave(session, cancellationToken); if (group.IsGroupEmpty()) { _logger.LogInformation("LeaveGroup: removing empty group {0}.", group.GetGroupId()); _groups.Remove(group.GetGroupId(), out _); } } } /// public List ListGroups(SessionInfo session, Guid filterItemId) { var user = _userManager.GetUserById(session.UserId); if (user.SyncPlayAccess == SyncPlayAccess.None) { return new List(); } // Filter by item if requested if (!filterItemId.Equals(Guid.Empty)) { return _groups.Values.Where( group => group.GetPlayingItemId().Equals(filterItemId) && HasAccessToItem(user, group.GetPlayingItemId())).Select( group => group.GetInfo()).ToList(); } else { // Otherwise show all available groups return _groups.Values.Where( group => HasAccessToItem(user, group.GetPlayingItemId())).Select( group => group.GetInfo()).ToList(); } } /// public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) { var user = _userManager.GetUserById(session.UserId); if (user.SyncPlayAccess == SyncPlayAccess.None) { _logger.LogWarning("HandleRequest: {0} does not have access to SyncPlay.", session.Id); var error = new GroupUpdate() { Type = GroupUpdateType.JoinGroupDenied }; _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); return; } lock (_groupsLock) { _sessionToGroupMap.TryGetValue(session.Id, out var group); if (group == null) { _logger.LogWarning("HandleRequest: {0} does not belong to any group.", session.Id); var error = new GroupUpdate() { Type = GroupUpdateType.NotInGroup }; _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); return; } group.HandleRequest(session, request, cancellationToken); } } /// public void AddSessionToGroup(SessionInfo session, ISyncPlayController group) { if (IsSessionInGroup(session)) { throw new InvalidOperationException("Session in other group already!"); } _sessionToGroupMap[session.Id] = group; } /// public void RemoveSessionFromGroup(SessionInfo session, ISyncPlayController group) { if (!IsSessionInGroup(session)) { throw new InvalidOperationException("Session not in any group!"); } _sessionToGroupMap.Remove(session.Id, out var tempGroup); if (!tempGroup.GetGroupId().Equals(group.GetGroupId())) { throw new InvalidOperationException("Session was in wrong group!"); } } } }