using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Library; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Session; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.Session { /// /// Class SessionManager /// public class SessionManager : ISessionManager { /// /// The _user data repository /// private readonly IUserDataManager _userDataRepository; /// /// The _user repository /// private readonly IUserRepository _userRepository; /// /// The _logger /// private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; private readonly IMusicManager _musicManager; private readonly IDtoService _dtoService; private readonly IImageProcessor _imageProcessor; private readonly IItemRepository _itemRepo; private readonly IHttpClient _httpClient; private readonly IJsonSerializer _jsonSerializer; private readonly IServerApplicationHost _appHost; /// /// Gets or sets the configuration manager. /// /// The configuration manager. private readonly IServerConfigurationManager _configurationManager; /// /// The _active connections /// private readonly ConcurrentDictionary _activeConnections = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); /// /// Occurs when [playback start]. /// public event EventHandler PlaybackStart; /// /// Occurs when [playback progress]. /// public event EventHandler PlaybackProgress; /// /// Occurs when [playback stopped]. /// public event EventHandler PlaybackStopped; public event EventHandler SessionStarted; public event EventHandler CapabilitiesChanged; public event EventHandler SessionEnded; public event EventHandler SessionActivity; private IEnumerable _sessionFactories = new List(); private readonly SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1); /// /// Initializes a new instance of the class. /// /// The user data repository. /// The configuration manager. /// The logger. /// The user repository. /// The library manager. public SessionManager(IUserDataManager userDataRepository, IServerConfigurationManager configurationManager, ILogger logger, IUserRepository userRepository, ILibraryManager libraryManager, IUserManager userManager, IMusicManager musicManager, IDtoService dtoService, IImageProcessor imageProcessor, IItemRepository itemRepo, IJsonSerializer jsonSerializer, IServerApplicationHost appHost, IHttpClient httpClient) { _userDataRepository = userDataRepository; _configurationManager = configurationManager; _logger = logger; _userRepository = userRepository; _libraryManager = libraryManager; _userManager = userManager; _musicManager = musicManager; _dtoService = dtoService; _imageProcessor = imageProcessor; _itemRepo = itemRepo; _jsonSerializer = jsonSerializer; _appHost = appHost; _httpClient = httpClient; } /// /// Adds the parts. /// /// The session factories. public void AddParts(IEnumerable sessionFactories) { _sessionFactories = sessionFactories.ToList(); } /// /// Gets all connections. /// /// All connections. public IEnumerable Sessions { get { return _activeConnections.Values.OrderByDescending(c => c.LastActivityDate).ToList(); } } private void OnSessionStarted(SessionInfo info) { EventHelper.QueueEventIfNotNull(SessionStarted, this, new SessionEventArgs { SessionInfo = info }, _logger); if (!string.IsNullOrWhiteSpace(info.DeviceId)) { var capabilities = GetSavedCapabilities(info.DeviceId); if (capabilities != null) { ReportCapabilities(info, capabilities, false); } } } private async void OnSessionEnded(SessionInfo info) { try { await SendSessionEndedNotification(info, CancellationToken.None).ConfigureAwait(false); } catch (Exception ex) { _logger.ErrorException("Error in SendSessionEndedNotification", ex); } EventHelper.QueueEventIfNotNull(SessionEnded, this, new SessionEventArgs { SessionInfo = info }, _logger); var disposable = info.SessionController as IDisposable; if (disposable != null) { _logger.Debug("Disposing session controller {0}", disposable.GetType().Name); try { disposable.Dispose(); } catch (Exception ex) { _logger.ErrorException("Error disposing session controller", ex); } } } /// /// Logs the user activity. /// /// Type of the client. /// The app version. /// The device id. /// Name of the device. /// The remote end point. /// The user. /// Task. /// user /// public async Task LogSessionActivity(string clientType, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user) { if (string.IsNullOrEmpty(clientType)) { throw new ArgumentNullException("clientType"); } if (string.IsNullOrEmpty(appVersion)) { throw new ArgumentNullException("appVersion"); } if (string.IsNullOrEmpty(deviceId)) { throw new ArgumentNullException("deviceId"); } if (string.IsNullOrEmpty(deviceName)) { throw new ArgumentNullException("deviceName"); } if (user != null && user.Configuration.IsDisabled) { throw new UnauthorizedAccessException(string.Format("The {0} account is currently disabled. Please consult with your administrator.", user.Name)); } var activityDate = DateTime.UtcNow; var userId = user == null ? (Guid?)null : user.Id; var username = user == null ? null : user.Name; var session = await GetSessionInfo(clientType, appVersion, deviceId, deviceName, remoteEndPoint, userId, username).ConfigureAwait(false); session.LastActivityDate = activityDate; if (user == null) { return session; } var lastActivityDate = user.LastActivityDate; user.LastActivityDate = activityDate; // Don't log in the db anymore frequently than 10 seconds if (lastActivityDate.HasValue && (activityDate - lastActivityDate.Value).TotalSeconds < 10) { return session; } // Save this directly. No need to fire off all the events for this. await _userRepository.SaveUser(user, CancellationToken.None).ConfigureAwait(false); EventHelper.FireEventIfNotNull(SessionActivity, this, new SessionEventArgs { SessionInfo = session }, _logger); return session; } public async void ReportSessionEnded(string sessionId) { await _sessionLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); try { var session = GetSession(sessionId, false); if (session != null) { var key = GetSessionKey(session.Client, session.ApplicationVersion, session.DeviceId); SessionInfo removed; if (_activeConnections.TryRemove(key, out removed)) { OnSessionEnded(removed); } } } finally { _sessionLock.Release(); } } /// /// Updates the now playing item id. /// /// The session. /// The information. /// The library item. private void UpdateNowPlayingItem(SessionInfo session, PlaybackProgressInfo info, BaseItem libraryItem) { var runtimeTicks = libraryItem == null ? null : libraryItem.RunTimeTicks; if (string.IsNullOrWhiteSpace(info.MediaSourceId)) { info.MediaSourceId = info.ItemId; } if (!string.Equals(info.ItemId, info.MediaSourceId) && !string.IsNullOrWhiteSpace(info.MediaSourceId)) { runtimeTicks = _libraryManager.GetItemById(new Guid(info.MediaSourceId)).RunTimeTicks; } if (!string.IsNullOrWhiteSpace(info.ItemId) && libraryItem != null) { var current = session.NowPlayingItem; if (current == null || !string.Equals(current.Id, info.ItemId, StringComparison.OrdinalIgnoreCase)) { info.Item = GetItemInfo(libraryItem, libraryItem, info.MediaSourceId); } else { info.Item = current; } info.Item.RunTimeTicks = runtimeTicks; } session.NowPlayingItem = info.Item; session.LastActivityDate = DateTime.UtcNow; session.PlayState.IsPaused = info.IsPaused; session.PlayState.PositionTicks = info.PositionTicks; session.PlayState.MediaSourceId = info.MediaSourceId; session.PlayState.CanSeek = info.CanSeek; session.PlayState.IsMuted = info.IsMuted; session.PlayState.VolumeLevel = info.VolumeLevel; session.PlayState.AudioStreamIndex = info.AudioStreamIndex; session.PlayState.SubtitleStreamIndex = info.SubtitleStreamIndex; session.PlayState.PlayMethod = info.PlayMethod; } /// /// Removes the now playing item id. /// /// The session. /// item private void RemoveNowPlayingItem(SessionInfo session) { session.NowPlayingItem = null; session.PlayState = new PlayerStateInfo(); } private string GetSessionKey(string clientType, string appVersion, string deviceId) { return clientType + deviceId + appVersion; } /// /// Gets the connection. /// /// Type of the client. /// The app version. /// The device id. /// Name of the device. /// The remote end point. /// The user identifier. /// The username. /// SessionInfo. private async Task GetSessionInfo(string clientType, string appVersion, string deviceId, string deviceName, string remoteEndPoint, Guid? userId, string username) { var key = GetSessionKey(clientType, appVersion, deviceId); await _sessionLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); try { var connection = _activeConnections.GetOrAdd(key, keyName => { var sessionInfo = new SessionInfo { Client = clientType, DeviceId = deviceId, ApplicationVersion = appVersion, Id = Guid.NewGuid().ToString("N") }; OnSessionStarted(sessionInfo); return sessionInfo; }); connection.DeviceName = deviceName; connection.UserId = userId; connection.UserName = username; connection.RemoteEndPoint = remoteEndPoint; if (!userId.HasValue) { connection.AdditionalUsers.Clear(); } if (connection.SessionController == null) { connection.SessionController = _sessionFactories .Select(i => i.GetSessionController(connection)) .FirstOrDefault(i => i != null); } return connection; } finally { _sessionLock.Release(); } } private List GetUsers(SessionInfo session) { var users = new List(); if (session.UserId.HasValue) { var user = _userManager.GetUserById(session.UserId.Value); if (user == null) { throw new InvalidOperationException("User not found"); } users.Add(user); var additionalUsers = session.AdditionalUsers .Select(i => _userManager.GetUserById(new Guid(i.UserId))) .Where(i => i != null); users.AddRange(additionalUsers); } return users; } /// /// Used to report that playback has started for an item /// /// The info. /// Task. /// info public async Task OnPlaybackStart(PlaybackStartInfo info) { if (info == null) { throw new ArgumentNullException("info"); } var session = GetSession(info.SessionId); var libraryItem = string.IsNullOrWhiteSpace(info.ItemId) ? null : _libraryManager.GetItemById(new Guid(info.ItemId)); UpdateNowPlayingItem(session, info, libraryItem); session.QueueableMediaTypes = info.QueueableMediaTypes; var users = GetUsers(session); if (libraryItem != null) { var key = libraryItem.GetUserDataKey(); foreach (var user in users) { await OnPlaybackStart(user.Id, key, libraryItem).ConfigureAwait(false); } } // Nothing to save here // Fire events to inform plugins EventHelper.QueueEventIfNotNull(PlaybackStart, this, new PlaybackProgressEventArgs { Item = libraryItem, Users = users, MediaSourceId = info.MediaSourceId, MediaInfo = info.Item, DeviceName = session.DeviceName, ClientName = session.Client, DeviceId = session.DeviceId }, _logger); await SendPlaybackStartNotification(session, CancellationToken.None).ConfigureAwait(false); } /// /// Called when [playback start]. /// /// The user identifier. /// The user data key. /// The item. /// Task. private async Task OnPlaybackStart(Guid userId, string userDataKey, IHasUserData item) { var data = _userDataRepository.GetUserData(userId, userDataKey); data.PlayCount++; data.LastPlayedDate = DateTime.UtcNow; if (!(item is Video)) { data.Played = true; } await _userDataRepository.SaveUserData(userId, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None).ConfigureAwait(false); } /// /// Used to report playback progress for an item /// /// The info. /// Task. /// /// positionTicks public async Task OnPlaybackProgress(PlaybackProgressInfo info) { if (info == null) { throw new ArgumentNullException("info"); } var session = GetSession(info.SessionId); var libraryItem = string.IsNullOrWhiteSpace(info.ItemId) ? null : _libraryManager.GetItemById(new Guid(info.ItemId)); UpdateNowPlayingItem(session, info, libraryItem); var users = GetUsers(session); if (libraryItem != null) { var key = libraryItem.GetUserDataKey(); foreach (var user in users) { await OnPlaybackProgress(user.Id, key, libraryItem, info.PositionTicks).ConfigureAwait(false); } } EventHelper.FireEventIfNotNull(PlaybackProgress, this, new PlaybackProgressEventArgs { Item = libraryItem, Users = users, PlaybackPositionTicks = session.PlayState.PositionTicks, MediaSourceId = session.PlayState.MediaSourceId, MediaInfo = info.Item, DeviceName = session.DeviceName, ClientName = session.Client, DeviceId = session.DeviceId }, _logger); } private async Task OnPlaybackProgress(Guid userId, string userDataKey, BaseItem item, long? positionTicks) { var data = _userDataRepository.GetUserData(userId, userDataKey); if (positionTicks.HasValue) { UpdatePlayState(item, data, positionTicks.Value); await _userDataRepository.SaveUserData(userId, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.None).ConfigureAwait(false); } } /// /// Used to report that playback has ended for an item /// /// The info. /// Task. /// info /// positionTicks public async Task OnPlaybackStopped(PlaybackStopInfo info) { if (info == null) { throw new ArgumentNullException("info"); } if (info.PositionTicks.HasValue && info.PositionTicks.Value < 0) { throw new ArgumentOutOfRangeException("positionTicks"); } var session = GetSession(info.SessionId); var libraryItem = string.IsNullOrWhiteSpace(info.ItemId) ? null : _libraryManager.GetItemById(new Guid(info.ItemId)); // Normalize if (string.IsNullOrWhiteSpace(info.MediaSourceId)) { info.MediaSourceId = info.ItemId; } RemoveNowPlayingItem(session); var users = GetUsers(session); var playedToCompletion = false; if (libraryItem != null) { var key = libraryItem.GetUserDataKey(); foreach (var user in users) { playedToCompletion = await OnPlaybackStopped(user.Id, key, libraryItem, info.PositionTicks).ConfigureAwait(false); } } EventHelper.QueueEventIfNotNull(PlaybackStopped, this, new PlaybackStopEventArgs { Item = libraryItem, Users = users, PlaybackPositionTicks = info.PositionTicks, PlayedToCompletion = playedToCompletion, MediaSourceId = info.MediaSourceId, MediaInfo = info.Item, DeviceName = session.DeviceName, ClientName = session.Client, DeviceId = session.DeviceId }, _logger); await SendPlaybackStoppedNotification(session, CancellationToken.None).ConfigureAwait(false); } private async Task OnPlaybackStopped(Guid userId, string userDataKey, BaseItem item, long? positionTicks) { var data = _userDataRepository.GetUserData(userId, userDataKey); bool playedToCompletion; if (positionTicks.HasValue) { playedToCompletion = UpdatePlayState(item, data, positionTicks.Value); } else { // If the client isn't able to report this, then we'll just have to make an assumption data.PlayCount++; data.Played = true; data.PlaybackPositionTicks = 0; playedToCompletion = true; } await _userDataRepository.SaveUserData(userId, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None).ConfigureAwait(false); return playedToCompletion; } /// /// Updates playstate position for an item but does not save /// /// The item /// User data for the item /// The current playback position private bool UpdatePlayState(BaseItem item, UserItemData data, long positionTicks) { var playedToCompletion = false; var hasRuntime = item.RunTimeTicks.HasValue && item.RunTimeTicks > 0; // If a position has been reported, and if we know the duration if (positionTicks > 0 && hasRuntime) { var pctIn = Decimal.Divide(positionTicks, item.RunTimeTicks.Value) * 100; // Don't track in very beginning if (pctIn < _configurationManager.Configuration.MinResumePct) { positionTicks = 0; } // If we're at the end, assume completed else if (pctIn > _configurationManager.Configuration.MaxResumePct || positionTicks >= item.RunTimeTicks.Value) { positionTicks = 0; data.Played = playedToCompletion = true; } else { // Enforce MinResumeDuration var durationSeconds = TimeSpan.FromTicks(item.RunTimeTicks.Value).TotalSeconds; if (durationSeconds < _configurationManager.Configuration.MinResumeDurationSeconds) { positionTicks = 0; data.Played = playedToCompletion = true; } } } else if (!hasRuntime) { // If we don't know the runtime we'll just have to assume it was fully played data.Played = playedToCompletion = true; positionTicks = 0; } if (item is Audio) { positionTicks = 0; } data.PlaybackPositionTicks = positionTicks; return playedToCompletion; } /// /// Gets the session. /// /// The session identifier. /// if set to true [throw on missing]. /// SessionInfo. /// private SessionInfo GetSession(string sessionId, bool throwOnMissing = true) { var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId)); if (session == null && throwOnMissing) { throw new ResourceNotFoundException(string.Format("Session {0} not found.", sessionId)); } return session; } public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, CancellationToken cancellationToken) { var generalCommand = new GeneralCommand { Name = GeneralCommandType.DisplayMessage.ToString() }; generalCommand.Arguments["Header"] = command.Header; generalCommand.Arguments["Text"] = command.Text; if (command.TimeoutMs.HasValue) { generalCommand.Arguments["TimeoutMs"] = command.TimeoutMs.Value.ToString(CultureInfo.InvariantCulture); } return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken); } public Task SendGeneralCommand(string controllingSessionId, string sessionId, GeneralCommand command, CancellationToken cancellationToken) { var session = GetSession(sessionId); var controllingSession = GetSession(controllingSessionId); AssertCanControl(session, controllingSession); return session.SessionController.SendGeneralCommand(command, cancellationToken); } public Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken) { var session = GetSession(sessionId); var user = session.UserId.HasValue ? _userManager.GetUserById(session.UserId.Value) : null; List items; if (command.PlayCommand == PlayCommand.PlayInstantMix) { items = command.ItemIds.SelectMany(i => TranslateItemForInstantMix(i, user)) .Where(i => i.LocationType != LocationType.Virtual) .ToList(); command.PlayCommand = PlayCommand.PlayNow; } else { items = command.ItemIds.SelectMany(i => TranslateItemForPlayback(i, user)) .Where(i => i.LocationType != LocationType.Virtual) .ToList(); } if (command.PlayCommand == PlayCommand.PlayShuffle) { items = items.OrderBy(i => Guid.NewGuid()).ToList(); command.PlayCommand = PlayCommand.PlayNow; } command.ItemIds = items.Select(i => i.Id.ToString("N")).ToArray(); if (user != null) { if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full)) { throw new ArgumentException(string.Format("{0} is not allowed to play media.", user.Name)); } } if (command.PlayCommand != PlayCommand.PlayNow) { if (items.Any(i => !session.QueueableMediaTypes.Contains(i.MediaType, StringComparer.OrdinalIgnoreCase))) { throw new ArgumentException(string.Format("{0} is unable to queue the requested media type.", session.DeviceName ?? session.Id.ToString())); } } else { if (items.Any(i => !session.PlayableMediaTypes.Contains(i.MediaType, StringComparer.OrdinalIgnoreCase))) { throw new ArgumentException(string.Format("{0} is unable to play the requested media type.", session.DeviceName ?? session.Id.ToString())); } } var controllingSession = GetSession(controllingSessionId); AssertCanControl(session, controllingSession); if (controllingSession.UserId.HasValue) { command.ControllingUserId = controllingSession.UserId.Value.ToString("N"); } return session.SessionController.SendPlayCommand(command, cancellationToken); } private IEnumerable TranslateItemForPlayback(string id, User user) { var item = _libraryManager.GetItemById(new Guid(id)); if (item.IsFolder) { var folder = (Folder)item; var items = user == null ? folder.RecursiveChildren : folder.GetRecursiveChildren(user); items = items.Where(i => !i.IsFolder); items = items.OrderBy(i => i.SortName); return items; } return new[] { item }; } private IEnumerable TranslateItemForInstantMix(string id, User user) { var item = _libraryManager.GetItemById(new Guid(id)); var audio = item as Audio; if (audio != null) { return _musicManager.GetInstantMixFromSong(audio, user); } var artist = item as MusicArtist; if (artist != null) { return _musicManager.GetInstantMixFromArtist(artist.Name, user); } var album = item as MusicAlbum; if (album != null) { return _musicManager.GetInstantMixFromAlbum(album, user); } var genre = item as MusicGenre; if (genre != null) { return _musicManager.GetInstantMixFromGenres(new[] { genre.Name }, user); } return new BaseItem[] { }; } public Task SendBrowseCommand(string controllingSessionId, string sessionId, BrowseRequest command, CancellationToken cancellationToken) { var generalCommand = new GeneralCommand { Name = GeneralCommandType.DisplayContent.ToString() }; generalCommand.Arguments["ItemId"] = command.ItemId; generalCommand.Arguments["ItemName"] = command.ItemName; generalCommand.Arguments["ItemType"] = command.ItemType; return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken); } public Task SendPlaystateCommand(string controllingSessionId, string sessionId, PlaystateRequest command, CancellationToken cancellationToken) { var session = GetSession(sessionId); var controllingSession = GetSession(controllingSessionId); AssertCanControl(session, controllingSession); if (controllingSession.UserId.HasValue) { command.ControllingUserId = controllingSession.UserId.Value.ToString("N"); } return session.SessionController.SendPlaystateCommand(command, cancellationToken); } private void AssertCanControl(SessionInfo session, SessionInfo controllingSession) { if (session == null) { throw new ArgumentNullException("session"); } if (controllingSession == null) { throw new ArgumentNullException("controllingSession"); } } /// /// Sends the restart required message. /// /// The cancellation token. /// Task. public Task SendRestartRequiredNotification(CancellationToken cancellationToken) { var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null).ToList(); var info = _appHost.GetSystemInfo(); var tasks = sessions.Select(session => Task.Run(async () => { try { await session.SessionController.SendRestartRequiredNotification(info, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { _logger.ErrorException("Error in SendRestartRequiredNotification.", ex); } }, cancellationToken)); return Task.WhenAll(tasks); } /// /// Sends the server shutdown notification. /// /// The cancellation token. /// Task. public Task SendServerShutdownNotification(CancellationToken cancellationToken) { var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null).ToList(); var tasks = sessions.Select(session => Task.Run(async () => { try { await session.SessionController.SendServerShutdownNotification(cancellationToken).ConfigureAwait(false); } catch (Exception ex) { _logger.ErrorException("Error in SendServerShutdownNotification.", ex); } }, cancellationToken)); return Task.WhenAll(tasks); } /// /// Sends the server restart notification. /// /// The cancellation token. /// Task. public Task SendServerRestartNotification(CancellationToken cancellationToken) { _logger.Debug("Beginning SendServerRestartNotification"); var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null).ToList(); var tasks = sessions.Select(session => Task.Run(async () => { try { await session.SessionController.SendServerRestartNotification(cancellationToken).ConfigureAwait(false); } catch (Exception ex) { _logger.ErrorException("Error in SendServerRestartNotification.", ex); } }, cancellationToken)); return Task.WhenAll(tasks); } public Task SendSessionEndedNotification(SessionInfo sessionInfo, CancellationToken cancellationToken) { var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null).ToList(); var dto = GetSessionInfoDto(sessionInfo); var tasks = sessions.Select(session => Task.Run(async () => { try { await session.SessionController.SendSessionEndedNotification(dto, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { _logger.ErrorException("Error in SendSessionEndedNotification.", ex); } }, cancellationToken)); return Task.WhenAll(tasks); } public Task SendPlaybackStartNotification(SessionInfo sessionInfo, CancellationToken cancellationToken) { var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null).ToList(); var dto = GetSessionInfoDto(sessionInfo); var tasks = sessions.Select(session => Task.Run(async () => { try { await session.SessionController.SendPlaybackStartNotification(dto, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { _logger.ErrorException("Error in SendPlaybackStartNotification.", ex); } }, cancellationToken)); return Task.WhenAll(tasks); } public Task SendPlaybackStoppedNotification(SessionInfo sessionInfo, CancellationToken cancellationToken) { var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null).ToList(); var dto = GetSessionInfoDto(sessionInfo); var tasks = sessions.Select(session => Task.Run(async () => { try { await session.SessionController.SendPlaybackStoppedNotification(dto, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { _logger.ErrorException("Error in SendPlaybackStoppedNotification.", ex); } }, cancellationToken)); return Task.WhenAll(tasks); } /// /// Adds the additional user. /// /// The session identifier. /// The user identifier. /// Cannot modify additional users without authenticating first. /// The requested user is already the primary user of the session. public void AddAdditionalUser(string sessionId, Guid userId) { var session = GetSession(sessionId); if (!session.UserId.HasValue) { throw new UnauthorizedAccessException("Cannot modify additional users without authenticating first."); } if (session.UserId.Value == userId) { throw new ArgumentException("The requested user is already the primary user of the session."); } if (session.AdditionalUsers.All(i => new Guid(i.UserId) != userId)) { var user = _userManager.GetUserById(userId); session.AdditionalUsers.Add(new SessionUserInfo { UserId = userId.ToString("N"), UserName = user.Name }); } } /// /// Removes the additional user. /// /// The session identifier. /// The user identifier. /// Cannot modify additional users without authenticating first. /// The requested user is already the primary user of the session. public void RemoveAdditionalUser(string sessionId, Guid userId) { var session = GetSession(sessionId); if (!session.UserId.HasValue) { throw new UnauthorizedAccessException("Cannot modify additional users without authenticating first."); } if (session.UserId.Value == userId) { throw new ArgumentException("The requested user is already the primary user of the session."); } var user = session.AdditionalUsers.FirstOrDefault(i => new Guid(i.UserId) == userId); if (user != null) { session.AdditionalUsers.Remove(user); } } /// /// Authenticates the new session. /// /// The user. /// The password. /// Type of the client. /// The application version. /// The device identifier. /// Name of the device. /// The remote end point. /// Task{SessionInfo}. /// public async Task AuthenticateNewSession(User user, string password, string clientType, string appVersion, string deviceId, string deviceName, string remoteEndPoint) { var result = await _userManager.AuthenticateUser(user, password).ConfigureAwait(false); if (!result) { throw new UnauthorizedAccessException("Invalid user or password entered."); } return await LogSessionActivity(clientType, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false); } /// /// Reports the capabilities. /// /// The session identifier. /// The capabilities. public void ReportCapabilities(string sessionId, SessionCapabilities capabilities) { var session = GetSession(sessionId); ReportCapabilities(session, capabilities, true); } private async void ReportCapabilities(SessionInfo session, SessionCapabilities capabilities, bool saveCapabilities) { session.PlayableMediaTypes = capabilities.PlayableMediaTypes; session.SupportedCommands = capabilities.SupportedCommands; if (!string.IsNullOrWhiteSpace(capabilities.MessageCallbackUrl)) { var controller = session.SessionController as HttpSessionController; if (controller == null) { session.SessionController = new HttpSessionController(_httpClient, _jsonSerializer, session, capabilities.MessageCallbackUrl, this); } } EventHelper.FireEventIfNotNull(CapabilitiesChanged, this, new SessionEventArgs { SessionInfo = session }, _logger); if (saveCapabilities) { await SaveCapabilities(session.DeviceId, capabilities).ConfigureAwait(false); } } private string GetCapabilitiesFilePath(string deviceId) { var filename = deviceId.GetMD5().ToString("N") + ".json"; return Path.Combine(_configurationManager.ApplicationPaths.CachePath, "devices", filename); } private SessionCapabilities GetSavedCapabilities(string deviceId) { var path = GetCapabilitiesFilePath(deviceId); try { return _jsonSerializer.DeserializeFromFile(path); } catch (DirectoryNotFoundException) { return null; } catch (FileNotFoundException) { return null; } catch (Exception ex) { _logger.ErrorException("Error getting saved capabilities", ex); return null; } } private readonly SemaphoreSlim _capabilitiesLock = new SemaphoreSlim(1, 1); private async Task SaveCapabilities(string deviceId, SessionCapabilities capabilities) { var path = GetCapabilitiesFilePath(deviceId); Directory.CreateDirectory(Path.GetDirectoryName(path)); await _capabilitiesLock.WaitAsync().ConfigureAwait(false); try { _jsonSerializer.SerializeToFile(capabilities, path); } finally { _capabilitiesLock.Release(); } } public SessionInfoDto GetSessionInfoDto(SessionInfo session) { var dto = new SessionInfoDto { Client = session.Client, DeviceId = session.DeviceId, DeviceName = session.DeviceName, Id = session.Id, LastActivityDate = session.LastActivityDate, NowPlayingPositionTicks = session.PlayState.PositionTicks, IsPaused = session.PlayState.IsPaused, IsMuted = session.PlayState.IsMuted, NowViewingItem = session.NowViewingItem, ApplicationVersion = session.ApplicationVersion, CanSeek = session.PlayState.CanSeek, QueueableMediaTypes = session.QueueableMediaTypes, PlayableMediaTypes = session.PlayableMediaTypes, RemoteEndPoint = session.RemoteEndPoint, AdditionalUsers = session.AdditionalUsers, SupportedCommands = session.SupportedCommands, UserName = session.UserName, NowPlayingItem = session.NowPlayingItem, SupportsRemoteControl = session.SupportsMediaControl, PlayState = session.PlayState }; if (session.UserId.HasValue) { dto.UserId = session.UserId.Value.ToString("N"); var user = _userManager.GetUserById(session.UserId.Value); if (user != null) { dto.UserPrimaryImageTag = GetImageCacheTag(user, ImageType.Primary); } } return dto; } /// /// Converts a BaseItem to a BaseItemInfo /// /// The item. /// The chapter owner. /// The media source identifier. /// BaseItemInfo. /// item private BaseItemInfo GetItemInfo(BaseItem item, BaseItem chapterOwner, string mediaSourceId) { if (item == null) { throw new ArgumentNullException("item"); } var info = new BaseItemInfo { Id = GetDtoId(item), Name = item.Name, MediaType = item.MediaType, Type = item.GetClientTypeName(), RunTimeTicks = item.RunTimeTicks, IndexNumber = item.IndexNumber, ParentIndexNumber = item.ParentIndexNumber, PremiereDate = item.PremiereDate, ProductionYear = item.ProductionYear }; info.PrimaryImageTag = GetImageCacheTag(item, ImageType.Primary); if (info.PrimaryImageTag != null) { info.PrimaryImageItemId = GetDtoId(item); } var episode = item as Episode; if (episode != null) { info.IndexNumberEnd = episode.IndexNumberEnd; } var hasSeries = item as IHasSeries; if (hasSeries != null) { info.SeriesName = hasSeries.SeriesName; } var recording = item as ILiveTvRecording; if (recording != null && recording.RecordingInfo != null) { if (recording.RecordingInfo.IsSeries) { info.Name = recording.RecordingInfo.EpisodeTitle; info.SeriesName = recording.RecordingInfo.Name; if (string.IsNullOrWhiteSpace(info.Name)) { info.Name = recording.RecordingInfo.Name; } } } var audio = item as Audio; if (audio != null) { info.Album = audio.Album; info.Artists = audio.Artists; if (info.PrimaryImageTag == null) { var album = audio.Parents.OfType().FirstOrDefault(); if (album != null && album.HasImage(ImageType.Primary)) { info.PrimaryImageTag = GetImageCacheTag(album, ImageType.Primary); if (info.PrimaryImageTag != null) { info.PrimaryImageItemId = GetDtoId(album); } } } } var musicVideo = item as MusicVideo; if (musicVideo != null) { info.Album = musicVideo.Album; if (!string.IsNullOrWhiteSpace(musicVideo.Artist)) { info.Artists.Add(musicVideo.Artist); } } var backropItem = item.HasImage(ImageType.Backdrop) ? item : null; var thumbItem = item.HasImage(ImageType.Thumb) ? item : null; var logoItem = item.HasImage(ImageType.Logo) ? item : null; if (thumbItem == null) { if (episode != null) { var series = episode.Series; if (series != null && series.HasImage(ImageType.Thumb)) { thumbItem = series; } } } if (backropItem == null) { if (episode != null) { var series = episode.Series; if (series != null && series.HasImage(ImageType.Backdrop)) { backropItem = series; } } } if (backropItem == null) { backropItem = item.Parents.FirstOrDefault(i => i.HasImage(ImageType.Backdrop)); } if (thumbItem == null) { thumbItem = item.Parents.FirstOrDefault(i => i.HasImage(ImageType.Thumb)); } if (logoItem == null) { logoItem = item.Parents.FirstOrDefault(i => i.HasImage(ImageType.Logo)); } if (thumbItem != null) { info.ThumbImageTag = GetImageCacheTag(thumbItem, ImageType.Thumb); info.ThumbItemId = GetDtoId(thumbItem); } if (backropItem != null) { info.BackdropImageTag = GetImageCacheTag(backropItem, ImageType.Backdrop); info.BackdropItemId = GetDtoId(backropItem); } if (logoItem != null) { info.LogoImageTag = GetImageCacheTag(logoItem, ImageType.Logo); info.LogoItemId = GetDtoId(logoItem); } if (chapterOwner != null) { info.ChapterImagesItemId = chapterOwner.Id.ToString("N"); info.Chapters = _itemRepo.GetChapters(chapterOwner.Id).Select(i => _dtoService.GetChapterInfoDto(i, chapterOwner)).ToList(); } if (!string.IsNullOrWhiteSpace(mediaSourceId)) { info.MediaStreams = _itemRepo.GetMediaStreams(new MediaStreamQuery { ItemId = new Guid(mediaSourceId) }).ToList(); } return info; } private string GetImageCacheTag(BaseItem item, ImageType type) { try { return _imageProcessor.GetImageCacheTag(item, type); } catch (Exception ex) { _logger.ErrorException("Error getting {0} image info", ex, type); return null; } } private string GetDtoId(BaseItem item) { return _dtoService.GetDtoId(item); } public void ReportNowViewingItem(string sessionId, string itemId) { var item = _libraryManager.GetItemById(new Guid(itemId)); var info = GetItemInfo(item, null, null); ReportNowViewingItem(sessionId, info); } public void ReportNowViewingItem(string sessionId, BaseItemInfo item) { var session = GetSession(sessionId); session.NowViewingItem = item; } } }