diff options
| author | Luke Pulverenti <luke.pulverenti@gmail.com> | 2016-11-03 19:35:19 -0400 |
|---|---|---|
| committer | Luke Pulverenti <luke.pulverenti@gmail.com> | 2016-11-03 19:35:19 -0400 |
| commit | d5ea8ca3ad378fc7e0a18ad314e1dfce07003ab6 (patch) | |
| tree | 4742a665e3455389a9795ff8b6c292263b3876e8 /Emby.Server.Implementations/Session | |
| parent | d0babf322dad6624ee15622d11db52e58db5197f (diff) | |
move classes to portable
Diffstat (limited to 'Emby.Server.Implementations/Session')
4 files changed, 2892 insertions, 0 deletions
diff --git a/Emby.Server.Implementations/Session/HttpSessionController.cs b/Emby.Server.Implementations/Session/HttpSessionController.cs new file mode 100644 index 000000000..cea5d9b40 --- /dev/null +++ b/Emby.Server.Implementations/Session/HttpSessionController.cs @@ -0,0 +1,186 @@ +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.System; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace Emby.Server.Implementations.Session +{ + public class HttpSessionController : ISessionController, IDisposable + { + private readonly IHttpClient _httpClient; + private readonly IJsonSerializer _json; + private readonly ISessionManager _sessionManager; + + public SessionInfo Session { get; private set; } + + private readonly string _postUrl; + + public HttpSessionController(IHttpClient httpClient, + IJsonSerializer json, + SessionInfo session, + string postUrl, ISessionManager sessionManager) + { + _httpClient = httpClient; + _json = json; + Session = session; + _postUrl = postUrl; + _sessionManager = sessionManager; + } + + public void OnActivity() + { + } + + private string PostUrl + { + get + { + return string.Format("http://{0}{1}", Session.RemoteEndPoint, _postUrl); + } + } + + public bool IsSessionActive + { + get + { + return (DateTime.UtcNow - Session.LastActivityDate).TotalMinutes <= 10; + } + } + + public bool SupportsMediaControl + { + get { return true; } + } + + private Task SendMessage(string name, CancellationToken cancellationToken) + { + return SendMessage(name, new Dictionary<string, string>(), cancellationToken); + } + + private async Task SendMessage(string name, + Dictionary<string, string> args, + CancellationToken cancellationToken) + { + var url = PostUrl + "/" + name + ToQueryString(args); + + await _httpClient.Post(new HttpRequestOptions + { + Url = url, + CancellationToken = cancellationToken, + BufferContent = false + + }).ConfigureAwait(false); + } + + public Task SendSessionEndedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + public Task SendPlaybackStartNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + public Task SendPlaybackStoppedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken) + { + var dict = new Dictionary<string, string>(); + + dict["ItemIds"] = string.Join(",", command.ItemIds); + + if (command.StartPositionTicks.HasValue) + { + dict["StartPositionTicks"] = command.StartPositionTicks.Value.ToString(CultureInfo.InvariantCulture); + } + + return SendMessage(command.PlayCommand.ToString(), dict, cancellationToken); + } + + public Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken) + { + var args = new Dictionary<string, string>(); + + if (command.Command == PlaystateCommand.Seek) + { + if (!command.SeekPositionTicks.HasValue) + { + throw new ArgumentException("SeekPositionTicks cannot be null"); + } + + args["SeekPositionTicks"] = command.SeekPositionTicks.Value.ToString(CultureInfo.InvariantCulture); + } + + return SendMessage(command.Command.ToString(), args, cancellationToken); + } + + public Task SendLibraryUpdateInfo(LibraryUpdateInfo info, CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + public Task SendRestartRequiredNotification(SystemInfo info, CancellationToken cancellationToken) + { + return SendMessage("RestartRequired", cancellationToken); + } + + public Task SendUserDataChangeInfo(UserDataChangeInfo info, CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + public Task SendServerShutdownNotification(CancellationToken cancellationToken) + { + return SendMessage("ServerShuttingDown", cancellationToken); + } + + public Task SendServerRestartNotification(CancellationToken cancellationToken) + { + return SendMessage("ServerRestarting", cancellationToken); + } + + public Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken) + { + return SendMessage(command.Name, command.Arguments, cancellationToken); + } + + public Task SendMessage<T>(string name, T data, CancellationToken cancellationToken) + { + // Not supported or needed right now + return Task.FromResult(true); + } + + private string ToQueryString(Dictionary<string, string> nvc) + { + var array = (from item in nvc + select string.Format("{0}={1}", WebUtility.UrlEncode(item.Key), WebUtility.UrlEncode(item.Value))) + .ToArray(); + + var args = string.Join("&", array); + + if (string.IsNullOrEmpty(args)) + { + return args; + } + + return "?" + args; + } + + public void Dispose() + { + } + } +} diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs new file mode 100644 index 000000000..960fe0739 --- /dev/null +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -0,0 +1,1933 @@ +using MediaBrowser.Common.Events; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Devices; +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.Security; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Devices; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.Library; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.Users; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Threading; + +namespace Emby.Server.Implementations.Session +{ + /// <summary> + /// Class SessionManager + /// </summary> + public class SessionManager : ISessionManager + { + /// <summary> + /// The _user data repository + /// </summary> + private readonly IUserDataManager _userDataManager; + + /// <summary> + /// The _logger + /// </summary> + 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 IMediaSourceManager _mediaSourceManager; + + private readonly IHttpClient _httpClient; + private readonly IJsonSerializer _jsonSerializer; + private readonly IServerApplicationHost _appHost; + + private readonly IAuthenticationRepository _authRepo; + private readonly IDeviceManager _deviceManager; + private readonly ITimerFactory _timerFactory; + + /// <summary> + /// The _active connections + /// </summary> + private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections = + new ConcurrentDictionary<string, SessionInfo>(StringComparer.OrdinalIgnoreCase); + + public event EventHandler<GenericEventArgs<AuthenticationRequest>> AuthenticationFailed; + + public event EventHandler<GenericEventArgs<AuthenticationRequest>> AuthenticationSucceeded; + + /// <summary> + /// Occurs when [playback start]. + /// </summary> + public event EventHandler<PlaybackProgressEventArgs> PlaybackStart; + /// <summary> + /// Occurs when [playback progress]. + /// </summary> + public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress; + /// <summary> + /// Occurs when [playback stopped]. + /// </summary> + public event EventHandler<PlaybackStopEventArgs> PlaybackStopped; + + public event EventHandler<SessionEventArgs> SessionStarted; + public event EventHandler<SessionEventArgs> CapabilitiesChanged; + public event EventHandler<SessionEventArgs> SessionEnded; + public event EventHandler<SessionEventArgs> SessionActivity; + + private IEnumerable<ISessionControllerFactory> _sessionFactories = new List<ISessionControllerFactory>(); + + private readonly SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1); + + public SessionManager(IUserDataManager userDataManager, ILogger logger, ILibraryManager libraryManager, IUserManager userManager, IMusicManager musicManager, IDtoService dtoService, IImageProcessor imageProcessor, IJsonSerializer jsonSerializer, IServerApplicationHost appHost, IHttpClient httpClient, IAuthenticationRepository authRepo, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, ITimerFactory timerFactory) + { + _userDataManager = userDataManager; + _logger = logger; + _libraryManager = libraryManager; + _userManager = userManager; + _musicManager = musicManager; + _dtoService = dtoService; + _imageProcessor = imageProcessor; + _jsonSerializer = jsonSerializer; + _appHost = appHost; + _httpClient = httpClient; + _authRepo = authRepo; + _deviceManager = deviceManager; + _mediaSourceManager = mediaSourceManager; + _timerFactory = timerFactory; + + _deviceManager.DeviceOptionsUpdated += _deviceManager_DeviceOptionsUpdated; + } + + void _deviceManager_DeviceOptionsUpdated(object sender, GenericEventArgs<DeviceInfo> e) + { + foreach (var session in Sessions) + { + if (string.Equals(session.DeviceId, e.Argument.Id)) + { + session.DeviceName = e.Argument.Name; + } + } + } + + /// <summary> + /// Adds the parts. + /// </summary> + /// <param name="sessionFactories">The session factories.</param> + public void AddParts(IEnumerable<ISessionControllerFactory> sessionFactories) + { + _sessionFactories = sessionFactories.ToList(); + } + + /// <summary> + /// Gets all connections. + /// </summary> + /// <value>All connections.</value> + public IEnumerable<SessionInfo> 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) + { + info.AppIconUrl = capabilities.IconUrl; + 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); + } + } + } + + /// <summary> + /// Logs the user activity. + /// </summary> + /// <param name="appName">Type of the client.</param> + /// <param name="appVersion">The app version.</param> + /// <param name="deviceId">The device id.</param> + /// <param name="deviceName">Name of the device.</param> + /// <param name="remoteEndPoint">The remote end point.</param> + /// <param name="user">The user.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException">user</exception> + /// <exception cref="System.UnauthorizedAccessException"></exception> + public async Task<SessionInfo> LogSessionActivity(string appName, + string appVersion, + string deviceId, + string deviceName, + string remoteEndPoint, + User user) + { + if (string.IsNullOrEmpty(appName)) + { + throw new ArgumentNullException("appName"); + } + if (string.IsNullOrEmpty(appVersion)) + { + throw new ArgumentNullException("appVersion"); + } + if (string.IsNullOrEmpty(deviceId)) + { + throw new ArgumentNullException("deviceId"); + } + if (string.IsNullOrEmpty(deviceName)) + { + throw new ArgumentNullException("deviceName"); + } + + var activityDate = DateTime.UtcNow; + var session = await GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false); + var lastActivityDate = session.LastActivityDate; + session.LastActivityDate = activityDate; + + if (user != null) + { + var userLastActivityDate = user.LastActivityDate ?? DateTime.MinValue; + user.LastActivityDate = activityDate; + + if ((activityDate - userLastActivityDate).TotalSeconds > 60) + { + try + { + await _userManager.UpdateUser(user).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error updating user", ex); + } + } + } + + if ((activityDate - lastActivityDate).TotalSeconds > 10) + { + EventHelper.FireEventIfNotNull(SessionActivity, this, new SessionEventArgs + { + SessionInfo = session + + }, _logger); + } + + var controller = session.SessionController; + if (controller != null) + { + controller.OnActivity(); + } + + 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.DeviceId); + + SessionInfo removed; + _activeConnections.TryRemove(key, out removed); + + OnSessionEnded(session); + } + } + finally + { + _sessionLock.Release(); + } + } + + private Task<MediaSourceInfo> GetMediaSource(IHasMediaSources item, string mediaSourceId, string liveStreamId) + { + return _mediaSourceManager.GetMediaSource(item, mediaSourceId, liveStreamId, false, CancellationToken.None); + } + + /// <summary> + /// Updates the now playing item id. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="info">The information.</param> + /// <param name="libraryItem">The library item.</param> + private async Task UpdateNowPlayingItem(SessionInfo session, PlaybackProgressInfo info, BaseItem libraryItem) + { + if (string.IsNullOrWhiteSpace(info.MediaSourceId)) + { + info.MediaSourceId = info.ItemId; + } + + if (!string.IsNullOrWhiteSpace(info.ItemId) && info.Item == null && libraryItem != null) + { + var current = session.NowPlayingItem; + + if (current == null || !string.Equals(current.Id, info.ItemId, StringComparison.OrdinalIgnoreCase)) + { + var runtimeTicks = libraryItem.RunTimeTicks; + + MediaSourceInfo mediaSource = null; + var hasMediaSources = libraryItem as IHasMediaSources; + if (hasMediaSources != null) + { + mediaSource = await GetMediaSource(hasMediaSources, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false); + + if (mediaSource != null) + { + runtimeTicks = mediaSource.RunTimeTicks; + } + } + + info.Item = GetItemInfo(libraryItem, libraryItem, mediaSource); + + info.Item.RunTimeTicks = runtimeTicks; + } + else + { + info.Item = current; + } + } + + session.NowPlayingItem = info.Item; + session.LastActivityDate = DateTime.UtcNow; + session.LastPlaybackCheckIn = 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; + session.PlayState.RepeatMode = info.RepeatMode; + } + + /// <summary> + /// Removes the now playing item id. + /// </summary> + /// <param name="session">The session.</param> + /// <exception cref="System.ArgumentNullException">item</exception> + private void RemoveNowPlayingItem(SessionInfo session) + { + session.NowPlayingItem = null; + session.PlayState = new PlayerStateInfo(); + + if (!string.IsNullOrEmpty(session.DeviceId)) + { + ClearTranscodingInfo(session.DeviceId); + } + } + + private string GetSessionKey(string appName, string deviceId) + { + return appName + deviceId; + } + + /// <summary> + /// Gets the connection. + /// </summary> + /// <param name="appName">Type of the client.</param> + /// <param name="appVersion">The app version.</param> + /// <param name="deviceId">The device id.</param> + /// <param name="deviceName">Name of the device.</param> + /// <param name="remoteEndPoint">The remote end point.</param> + /// <param name="user">The user.</param> + /// <returns>SessionInfo.</returns> + private async Task<SessionInfo> GetSessionInfo(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user) + { + if (string.IsNullOrWhiteSpace(deviceId)) + { + throw new ArgumentNullException("deviceId"); + } + var key = GetSessionKey(appName, deviceId); + + await _sessionLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); + + var userId = user == null ? (Guid?)null : user.Id; + var username = user == null ? null : user.Name; + + try + { + SessionInfo sessionInfo; + DeviceInfo device = null; + + if (!_activeConnections.TryGetValue(key, out sessionInfo)) + { + sessionInfo = new SessionInfo + { + Client = appName, + DeviceId = deviceId, + ApplicationVersion = appVersion, + Id = key.GetMD5().ToString("N") + }; + + sessionInfo.DeviceName = deviceName; + sessionInfo.UserId = userId; + sessionInfo.UserName = username; + sessionInfo.RemoteEndPoint = remoteEndPoint; + + OnSessionStarted(sessionInfo); + + _activeConnections.TryAdd(key, sessionInfo); + + if (!string.IsNullOrEmpty(deviceId)) + { + var userIdString = userId.HasValue ? userId.Value.ToString("N") : null; + device = await _deviceManager.RegisterDevice(deviceId, deviceName, appName, appVersion, userIdString).ConfigureAwait(false); + } + } + + device = device ?? _deviceManager.GetDevice(deviceId); + + if (device == null) + { + var userIdString = userId.HasValue ? userId.Value.ToString("N") : null; + device = await _deviceManager.RegisterDevice(deviceId, deviceName, appName, appVersion, userIdString).ConfigureAwait(false); + } + + if (device != null) + { + if (!string.IsNullOrEmpty(device.CustomName)) + { + deviceName = device.CustomName; + } + } + + sessionInfo.DeviceName = deviceName; + sessionInfo.UserId = userId; + sessionInfo.UserName = username; + sessionInfo.RemoteEndPoint = remoteEndPoint; + sessionInfo.ApplicationVersion = appVersion; + + if (!userId.HasValue) + { + sessionInfo.AdditionalUsers.Clear(); + } + + if (sessionInfo.SessionController == null) + { + sessionInfo.SessionController = _sessionFactories + .Select(i => i.GetSessionController(sessionInfo)) + .FirstOrDefault(i => i != null); + } + + return sessionInfo; + } + finally + { + _sessionLock.Release(); + } + } + + private List<User> GetUsers(SessionInfo session) + { + var users = new List<User>(); + + 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(i.UserId)) + .Where(i => i != null); + + users.AddRange(additionalUsers); + } + + return users; + } + + private ITimer _idleTimer; + + private void StartIdleCheckTimer() + { + if (_idleTimer == null) + { + _idleTimer = _timerFactory.Create(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); + } + } + private void StopIdleCheckTimer() + { + if (_idleTimer != null) + { + _idleTimer.Dispose(); + _idleTimer = null; + } + } + + private async void CheckForIdlePlayback(object state) + { + var playingSessions = Sessions.Where(i => i.NowPlayingItem != null) + .ToList(); + + if (playingSessions.Count > 0) + { + var idle = playingSessions + .Where(i => (DateTime.UtcNow - i.LastPlaybackCheckIn).TotalMinutes > 5) + .ToList(); + + foreach (var session in idle) + { + _logger.Debug("Session {0} has gone idle while playing", session.Id); + + try + { + await OnPlaybackStopped(new PlaybackStopInfo + { + Item = session.NowPlayingItem, + ItemId = session.NowPlayingItem == null ? null : session.NowPlayingItem.Id, + SessionId = session.Id, + MediaSourceId = session.PlayState == null ? null : session.PlayState.MediaSourceId, + PositionTicks = session.PlayState == null ? null : session.PlayState.PositionTicks + }); + } + catch (Exception ex) + { + _logger.Debug("Error calling OnPlaybackStopped", ex); + } + } + + playingSessions = Sessions.Where(i => i.NowPlayingItem != null) + .ToList(); + } + + if (playingSessions.Count == 0) + { + StopIdleCheckTimer(); + } + } + + /// <summary> + /// Used to report that playback has started for an item + /// </summary> + /// <param name="info">The info.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException">info</exception> + 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)); + + await UpdateNowPlayingItem(session, info, libraryItem).ConfigureAwait(false); + + if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode) + { + ClearTranscodingInfo(session.DeviceId); + } + + session.QueueableMediaTypes = info.QueueableMediaTypes; + + var users = GetUsers(session); + + if (libraryItem != null) + { + foreach (var user in users) + { + await OnPlaybackStart(user.Id, 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); + + StartIdleCheckTimer(); + } + + /// <summary> + /// Called when [playback start]. + /// </summary> + /// <param name="userId">The user identifier.</param> + /// <param name="item">The item.</param> + /// <returns>Task.</returns> + private async Task OnPlaybackStart(Guid userId, IHasUserData item) + { + var data = _userDataManager.GetUserData(userId, item); + + data.PlayCount++; + data.LastPlayedDate = DateTime.UtcNow; + + if (item.SupportsPlayedStatus) + { + if (!(item is Video)) + { + data.Played = true; + } + } + else + { + data.Played = false; + } + + await _userDataManager.SaveUserData(userId, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None).ConfigureAwait(false); + } + + /// <summary> + /// Used to report playback progress for an item + /// </summary> + /// <param name="info">The info.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + /// <exception cref="System.ArgumentOutOfRangeException">positionTicks</exception> + 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)); + + await UpdateNowPlayingItem(session, info, libraryItem).ConfigureAwait(false); + + var users = GetUsers(session); + + if (libraryItem != null) + { + foreach (var user in users) + { + await OnPlaybackProgress(user, libraryItem, info).ConfigureAwait(false); + } + } + + if (!string.IsNullOrWhiteSpace(info.LiveStreamId)) + { + try + { + await _mediaSourceManager.PingLiveStream(info.LiveStreamId, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error closing live stream", ex); + } + } + + 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, + IsPaused = info.IsPaused, + PlaySessionId = info.PlaySessionId + + }, _logger); + + StartIdleCheckTimer(); + } + + private async Task OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info) + { + var data = _userDataManager.GetUserData(user.Id, item); + + var positionTicks = info.PositionTicks; + + if (positionTicks.HasValue) + { + _userDataManager.UpdatePlayState(item, data, positionTicks.Value); + + UpdatePlaybackSettings(user, info, data); + + await _userDataManager.SaveUserData(user.Id, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.None).ConfigureAwait(false); + } + } + + private void UpdatePlaybackSettings(User user, PlaybackProgressInfo info, UserItemData data) + { + if (user.Configuration.RememberAudioSelections) + { + data.AudioStreamIndex = info.AudioStreamIndex; + } + else + { + data.AudioStreamIndex = null; + } + + if (user.Configuration.RememberSubtitleSelections) + { + data.SubtitleStreamIndex = info.SubtitleStreamIndex; + } + else + { + data.SubtitleStreamIndex = null; + } + } + + /// <summary> + /// Used to report that playback has ended for an item + /// </summary> + /// <param name="info">The info.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException">info</exception> + /// <exception cref="System.ArgumentOutOfRangeException">positionTicks</exception> + 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; + } + + if (!string.IsNullOrWhiteSpace(info.ItemId) && info.Item == null && libraryItem != null) + { + var current = session.NowPlayingItem; + + if (current == null || !string.Equals(current.Id, info.ItemId, StringComparison.OrdinalIgnoreCase)) + { + MediaSourceInfo mediaSource = null; + + var hasMediaSources = libraryItem as IHasMediaSources; + if (hasMediaSources != null) + { + mediaSource = await GetMediaSource(hasMediaSources, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false); + } + + info.Item = GetItemInfo(libraryItem, libraryItem, mediaSource); + } + else + { + info.Item = current; + } + } + + RemoveNowPlayingItem(session); + + var users = GetUsers(session); + var playedToCompletion = false; + + if (libraryItem != null) + { + foreach (var user in users) + { + playedToCompletion = await OnPlaybackStopped(user.Id, libraryItem, info.PositionTicks, info.Failed).ConfigureAwait(false); + } + } + + if (!string.IsNullOrWhiteSpace(info.LiveStreamId)) + { + try + { + await _mediaSourceManager.CloseLiveStream(info.LiveStreamId).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error closing live stream", ex); + } + } + + 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<bool> OnPlaybackStopped(Guid userId, BaseItem item, long? positionTicks, bool playbackFailed) + { + bool playedToCompletion = false; + + if (!playbackFailed) + { + var data = _userDataManager.GetUserData(userId, item); + + if (positionTicks.HasValue) + { + playedToCompletion = _userDataManager.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 = item.SupportsPlayedStatus; + data.PlaybackPositionTicks = 0; + playedToCompletion = true; + } + + await _userDataManager.SaveUserData(userId, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None).ConfigureAwait(false); + } + + return playedToCompletion; + } + + /// <summary> + /// Gets the session. + /// </summary> + /// <param name="sessionId">The session identifier.</param> + /// <param name="throwOnMissing">if set to <c>true</c> [throw on missing].</param> + /// <returns>SessionInfo.</returns> + /// <exception cref="ResourceNotFoundException"></exception> + 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; + } + + private SessionInfo GetSessionToRemoteControl(string sessionId) + { + // Accept either device id or session id + var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId)); + + if (session == null) + { + 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 = GetSessionToRemoteControl(sessionId); + + var controllingSession = GetSession(controllingSessionId); + AssertCanControl(session, controllingSession); + + return session.SessionController.SendGeneralCommand(command, cancellationToken); + } + + public async Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken) + { + var session = GetSessionToRemoteControl(sessionId); + + var user = session.UserId.HasValue ? _userManager.GetUserById(session.UserId.Value) : null; + + List<BaseItem> 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 + { + var list = new List<BaseItem>(); + foreach (var itemId in command.ItemIds) + { + var subItems = await TranslateItemForPlayback(itemId, user).ConfigureAwait(false); + list.AddRange(subItems); + } + + items = list + .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)); + } + } + 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)); + } + } + + if (user != null && command.ItemIds.Length == 1 && user.Configuration.EnableNextEpisodeAutoPlay) + { + var episode = _libraryManager.GetItemById(command.ItemIds[0]) as Episode; + if (episode != null) + { + var series = episode.Series; + if (series != null) + { + var episodes = series.GetEpisodes(user) + .Where(i => !i.IsVirtualItem) + .SkipWhile(i => i.Id != episode.Id) + .ToList(); + + if (episodes.Count > 0) + { + command.ItemIds = episodes.Select(i => i.Id.ToString("N")).ToArray(); + } + } + } + } + + var controllingSession = GetSession(controllingSessionId); + AssertCanControl(session, controllingSession); + if (controllingSession.UserId.HasValue) + { + command.ControllingUserId = controllingSession.UserId.Value.ToString("N"); + } + + await session.SessionController.SendPlayCommand(command, cancellationToken).ConfigureAwait(false); + } + + private async Task<List<BaseItem>> TranslateItemForPlayback(string id, User user) + { + var item = _libraryManager.GetItemById(id); + + if (item == null) + { + _logger.Error("A non-existant item Id {0} was passed into TranslateItemForPlayback", id); + return new List<BaseItem>(); + } + + var byName = item as IItemByName; + + if (byName != null) + { + var items = byName.GetTaggedItems(new InternalItemsQuery(user) + { + IsFolder = false, + Recursive = true + }); + + return FilterToSingleMediaType(items) + .OrderBy(i => i.SortName) + .ToList(); + } + + if (item.IsFolder) + { + var folder = (Folder)item; + + var itemsResult = await folder.GetItems(new InternalItemsQuery(user) + { + Recursive = true, + IsFolder = false + + }).ConfigureAwait(false); + + return FilterToSingleMediaType(itemsResult.Items) + .OrderBy(i => i.SortName) + .ToList(); + } + + return new List<BaseItem> { item }; + } + + private IEnumerable<BaseItem> FilterToSingleMediaType(IEnumerable<BaseItem> items) + { + return items + .Where(i => !string.IsNullOrWhiteSpace(i.MediaType)) + .ToLookup(i => i.MediaType, StringComparer.OrdinalIgnoreCase) + .OrderByDescending(i => i.Count()) + .FirstOrDefault(); + } + + private IEnumerable<BaseItem> TranslateItemForInstantMix(string id, User user) + { + var item = _libraryManager.GetItemById(id); + + if (item == null) + { + _logger.Error("A non-existant item Id {0} was passed into TranslateItemForInstantMix", id); + return new List<BaseItem>(); + } + + return _musicManager.GetInstantMixFromItem(item, user); + } + + 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 = GetSessionToRemoteControl(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"); + } + } + + /// <summary> + /// Sends the restart required message. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public async Task SendRestartRequiredNotification(CancellationToken cancellationToken) + { + var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null).ToList(); + + var info = await _appHost.GetSystemInfo().ConfigureAwait(false); + + 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)); + + await Task.WhenAll(tasks).ConfigureAwait(false); + } + + /// <summary> + /// Sends the server shutdown notification. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + 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); + } + + /// <summary> + /// Sends the server restart notification. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + 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); + } + + /// <summary> + /// Adds the additional user. + /// </summary> + /// <param name="sessionId">The session identifier.</param> + /// <param name="userId">The user identifier.</param> + /// <exception cref="System.UnauthorizedAccessException">Cannot modify additional users without authenticating first.</exception> + /// <exception cref="System.ArgumentException">The requested user is already the primary user of the session.</exception> + public void AddAdditionalUser(string sessionId, string userId) + { + var session = GetSession(sessionId); + + if (session.UserId.HasValue && session.UserId.Value == new Guid(userId)) + { + throw new ArgumentException("The requested user is already the primary user of the session."); + } + + if (session.AdditionalUsers.All(i => new Guid(i.UserId) != new Guid(userId))) + { + var user = _userManager.GetUserById(userId); + + session.AdditionalUsers.Add(new SessionUserInfo + { + UserId = userId, + UserName = user.Name + }); + } + } + + /// <summary> + /// Removes the additional user. + /// </summary> + /// <param name="sessionId">The session identifier.</param> + /// <param name="userId">The user identifier.</param> + /// <exception cref="System.UnauthorizedAccessException">Cannot modify additional users without authenticating first.</exception> + /// <exception cref="System.ArgumentException">The requested user is already the primary user of the session.</exception> + public void RemoveAdditionalUser(string sessionId, string userId) + { + var session = GetSession(sessionId); + + if (session.UserId.HasValue && session.UserId.Value == new Guid(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) == new Guid(userId)); + + if (user != null) + { + session.AdditionalUsers.Remove(user); + } + } + + /// <summary> + /// Authenticates the new session. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>Task{SessionInfo}.</returns> + public Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request) + { + return AuthenticateNewSessionInternal(request, true); + } + + public Task<AuthenticationResult> CreateNewSession(AuthenticationRequest request) + { + return AuthenticateNewSessionInternal(request, false); + } + + private async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword) + { + User user = null; + if (!string.IsNullOrWhiteSpace(request.UserId)) + { + var idGuid = new Guid(request.UserId); + user = _userManager.Users + .FirstOrDefault(i => i.Id == idGuid); + } + + if (user == null) + { + user = _userManager.Users + .FirstOrDefault(i => string.Equals(request.Username, i.Name, StringComparison.OrdinalIgnoreCase)); + } + + if (user != null && !string.IsNullOrWhiteSpace(request.DeviceId)) + { + if (!_deviceManager.CanAccessDevice(user.Id.ToString("N"), request.DeviceId)) + { + throw new SecurityException("User is not allowed access from this device."); + } + } + + if (enforcePassword) + { + var result = await _userManager.AuthenticateUser(request.Username, request.PasswordSha1, request.PasswordMd5, request.RemoteEndPoint).ConfigureAwait(false); + + if (!result) + { + EventHelper.FireEventIfNotNull(AuthenticationFailed, this, new GenericEventArgs<AuthenticationRequest>(request), _logger); + + throw new SecurityException("Invalid user or password entered."); + } + } + + var token = await GetAuthorizationToken(user.Id.ToString("N"), request.DeviceId, request.App, request.AppVersion, request.DeviceName).ConfigureAwait(false); + + EventHelper.FireEventIfNotNull(AuthenticationSucceeded, this, new GenericEventArgs<AuthenticationRequest>(request), _logger); + + var session = await LogSessionActivity(request.App, + request.AppVersion, + request.DeviceId, + request.DeviceName, + request.RemoteEndPoint, + user) + .ConfigureAwait(false); + + return new AuthenticationResult + { + User = _userManager.GetUserDto(user, request.RemoteEndPoint), + SessionInfo = GetSessionInfoDto(session), + AccessToken = token, + ServerId = _appHost.SystemId + }; + } + + + private async Task<string> GetAuthorizationToken(string userId, string deviceId, string app, string appVersion, string deviceName) + { + var existing = _authRepo.Get(new AuthenticationInfoQuery + { + DeviceId = deviceId, + IsActive = true, + UserId = userId, + Limit = 1 + }); + + if (existing.Items.Length > 0) + { + var token = existing.Items[0].AccessToken; + _logger.Info("Reissuing access token: " + token); + return token; + } + + var newToken = new AuthenticationInfo + { + AppName = app, + AppVersion = appVersion, + DateCreated = DateTime.UtcNow, + DeviceId = deviceId, + DeviceName = deviceName, + UserId = userId, + IsActive = true, + AccessToken = Guid.NewGuid().ToString("N") + }; + + _logger.Info("Creating new access token for user {0}", userId); + await _authRepo.Create(newToken, CancellationToken.None).ConfigureAwait(false); + + return newToken.AccessToken; + } + + public async Task Logout(string accessToken) + { + if (string.IsNullOrWhiteSpace(accessToken)) + { + throw new ArgumentNullException("accessToken"); + } + + _logger.Info("Logging out access token {0}", accessToken); + + var existing = _authRepo.Get(new AuthenticationInfoQuery + { + Limit = 1, + AccessToken = accessToken + + }).Items.FirstOrDefault(); + + if (existing != null) + { + existing.IsActive = false; + + await _authRepo.Update(existing, CancellationToken.None).ConfigureAwait(false); + + var sessions = Sessions + .Where(i => string.Equals(i.DeviceId, existing.DeviceId, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + foreach (var session in sessions) + { + try + { + ReportSessionEnded(session.Id); + } + catch (Exception ex) + { + _logger.ErrorException("Error reporting session ended", ex); + } + } + } + } + + public async Task RevokeUserTokens(string userId, string currentAccessToken) + { + var existing = _authRepo.Get(new AuthenticationInfoQuery + { + IsActive = true, + UserId = userId + }); + + foreach (var info in existing.Items) + { + if (!string.Equals(currentAccessToken, info.AccessToken, StringComparison.OrdinalIgnoreCase)) + { + await Logout(info.AccessToken).ConfigureAwait(false); + } + } + } + + public Task RevokeToken(string token) + { + return Logout(token); + } + + /// <summary> + /// Reports the capabilities. + /// </summary> + /// <param name="sessionId">The session identifier.</param> + /// <param name="capabilities">The capabilities.</param> + public void ReportCapabilities(string sessionId, ClientCapabilities capabilities) + { + var session = GetSession(sessionId); + + ReportCapabilities(session, capabilities, true); + } + + private async void ReportCapabilities(SessionInfo session, + ClientCapabilities capabilities, + bool saveCapabilities) + { + session.Capabilities = capabilities; + + 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) + { + try + { + await SaveCapabilities(session.DeviceId, capabilities).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error saving device capabilities", ex); + } + } + } + + private ClientCapabilities GetSavedCapabilities(string deviceId) + { + return _deviceManager.GetCapabilities(deviceId); + } + + private Task SaveCapabilities(string deviceId, ClientCapabilities capabilities) + { + return _deviceManager.SaveCapabilities(deviceId, capabilities); + } + + public SessionInfoDto GetSessionInfoDto(SessionInfo session) + { + var dto = new SessionInfoDto + { + Client = session.Client, + DeviceId = session.DeviceId, + DeviceName = session.DeviceName, + Id = session.Id, + LastActivityDate = session.LastActivityDate, + NowViewingItem = session.NowViewingItem, + ApplicationVersion = session.ApplicationVersion, + QueueableMediaTypes = session.QueueableMediaTypes, + PlayableMediaTypes = session.PlayableMediaTypes, + AdditionalUsers = session.AdditionalUsers, + SupportedCommands = session.SupportedCommands, + UserName = session.UserName, + NowPlayingItem = session.NowPlayingItem, + SupportsRemoteControl = session.SupportsMediaControl, + PlayState = session.PlayState, + AppIconUrl = session.AppIconUrl, + TranscodingInfo = session.NowPlayingItem == null ? null : session.TranscodingInfo + }; + + 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; + } + + /// <summary> + /// Converts a BaseItem to a BaseItemInfo + /// </summary> + /// <param name="item">The item.</param> + /// <param name="chapterOwner">The chapter owner.</param> + /// <param name="mediaSource">The media source.</param> + /// <returns>BaseItemInfo.</returns> + /// <exception cref="System.ArgumentNullException">item</exception> + private BaseItemInfo GetItemInfo(BaseItem item, BaseItem chapterOwner, MediaSourceInfo mediaSource) + { + 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, + IsThemeMedia = item.IsThemeMedia + }; + + 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) + { + if (recording.IsSeries) + { + info.Name = recording.EpisodeTitle; + info.SeriesName = recording.Name; + + if (string.IsNullOrWhiteSpace(info.Name)) + { + info.Name = recording.Name; + } + } + } + + var audio = item as Audio; + if (audio != null) + { + info.Album = audio.Album; + info.Artists = audio.Artists; + + if (info.PrimaryImageTag == null) + { + var album = audio.AlbumEntity; + + 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; + info.Artists = musicVideo.Artists.ToList(); + } + + 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.GetParents().FirstOrDefault(i => i.HasImage(ImageType.Backdrop)); + } + + if (thumbItem == null) + { + thumbItem = item.GetParents().FirstOrDefault(i => i.HasImage(ImageType.Thumb)); + } + + if (logoItem == null) + { + logoItem = item.GetParents().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 = _dtoService.GetChapterInfoDtos(chapterOwner).ToList(); + } + + if (mediaSource != null) + { + info.MediaStreams = mediaSource.MediaStreams; + } + + 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) + { + if (string.IsNullOrWhiteSpace(itemId)) + { + throw new ArgumentNullException("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; + } + + public void ReportTranscodingInfo(string deviceId, TranscodingInfo info) + { + var session = Sessions.FirstOrDefault(i => string.Equals(i.DeviceId, deviceId)); + + if (session != null) + { + session.TranscodingInfo = info; + } + } + + public void ClearTranscodingInfo(string deviceId) + { + ReportTranscodingInfo(deviceId, null); + } + + public SessionInfo GetSession(string deviceId, string client, string version) + { + return Sessions.FirstOrDefault(i => string.Equals(i.DeviceId, deviceId) && + string.Equals(i.Client, client)); + } + + public Task<SessionInfo> GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion) + { + if (info == null) + { + throw new ArgumentNullException("info"); + } + + var user = string.IsNullOrWhiteSpace(info.UserId) + ? null + : _userManager.GetUserById(info.UserId); + + appVersion = string.IsNullOrWhiteSpace(appVersion) + ? info.AppVersion + : appVersion; + + var deviceName = info.DeviceName; + var appName = info.AppName; + + if (!string.IsNullOrWhiteSpace(deviceId)) + { + // Replace the info from the token with more recent info + var device = _deviceManager.GetDevice(deviceId); + if (device != null) + { + deviceName = device.Name; + appName = device.AppName; + + if (!string.IsNullOrWhiteSpace(device.AppVersion)) + { + appVersion = device.AppVersion; + } + } + } + else + { + deviceId = info.DeviceId; + } + + // Prevent argument exception + if (string.IsNullOrWhiteSpace(appVersion)) + { + appVersion = "1"; + } + + return LogSessionActivity(appName, appVersion, deviceId, deviceName, remoteEndpoint, user); + } + + public Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint) + { + var result = _authRepo.Get(new AuthenticationInfoQuery + { + AccessToken = token + }); + + var info = result.Items.FirstOrDefault(); + + if (info == null) + { + return Task.FromResult<SessionInfo>(null); + } + + return GetSessionByAuthenticationToken(info, deviceId, remoteEndpoint, null); + } + + public Task SendMessageToAdminSessions<T>(string name, T data, CancellationToken cancellationToken) + { + var adminUserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id.ToString("N")).ToList(); + + return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken); + } + + public Task SendMessageToUserSessions<T>(List<string> userIds, string name, T data, + CancellationToken cancellationToken) + { + var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null && userIds.Any(i.ContainsUser)).ToList(); + + var tasks = sessions.Select(session => Task.Run(async () => + { + try + { + await session.SessionController.SendMessage(name, data, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error sending message", ex); + } + + }, cancellationToken)); + + return Task.WhenAll(tasks); + } + + public Task SendMessageToUserDeviceSessions<T>(string deviceId, string name, T data, + CancellationToken cancellationToken) + { + var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null && string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)).ToList(); + + var tasks = sessions.Select(session => Task.Run(async () => + { + try + { + await session.SessionController.SendMessage(name, data, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error sending message", ex); + } + + }, cancellationToken)); + + return Task.WhenAll(tasks); + } + } +}
\ No newline at end of file diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs new file mode 100644 index 000000000..336c2caee --- /dev/null +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -0,0 +1,485 @@ +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Session; +using System; +using System.Collections.Specialized; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; + +namespace Emby.Server.Implementations.Session +{ + /// <summary> + /// Class SessionWebSocketListener + /// </summary> + public class SessionWebSocketListener : IWebSocketListener, IDisposable + { + /// <summary> + /// The _true task result + /// </summary> + private readonly Task _trueTaskResult = Task.FromResult(true); + + /// <summary> + /// The _session manager + /// </summary> + private readonly ISessionManager _sessionManager; + + /// <summary> + /// The _logger + /// </summary> + private readonly ILogger _logger; + + /// <summary> + /// The _dto service + /// </summary> + private readonly IJsonSerializer _json; + + private readonly IHttpServer _httpServer; + private readonly IServerManager _serverManager; + + + /// <summary> + /// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class. + /// </summary> + /// <param name="sessionManager">The session manager.</param> + /// <param name="logManager">The log manager.</param> + /// <param name="json">The json.</param> + /// <param name="httpServer">The HTTP server.</param> + /// <param name="serverManager">The server manager.</param> + public SessionWebSocketListener(ISessionManager sessionManager, ILogManager logManager, IJsonSerializer json, IHttpServer httpServer, IServerManager serverManager) + { + _sessionManager = sessionManager; + _logger = logManager.GetLogger(GetType().Name); + _json = json; + _httpServer = httpServer; + _serverManager = serverManager; + httpServer.WebSocketConnecting += _httpServer_WebSocketConnecting; + serverManager.WebSocketConnected += _serverManager_WebSocketConnected; + } + + async void _serverManager_WebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e) + { + var session = await GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint).ConfigureAwait(false); + + if (session != null) + { + var controller = session.SessionController as WebSocketController; + + if (controller == null) + { + controller = new WebSocketController(session, _logger, _sessionManager); + } + + controller.AddWebSocket(e.Argument); + + session.SessionController = controller; + } + else + { + _logger.Warn("Unable to determine session based on url: {0}", e.Argument.Url); + } + } + + async void _httpServer_WebSocketConnecting(object sender, WebSocketConnectingEventArgs e) + { + //var token = e.QueryString["api_key"]; + //if (!string.IsNullOrWhiteSpace(token)) + //{ + // try + // { + // var session = await GetSession(e.QueryString, e.Endpoint).ConfigureAwait(false); + + // if (session == null) + // { + // e.AllowConnection = false; + // } + // } + // catch (Exception ex) + // { + // _logger.ErrorException("Error getting session info", ex); + // } + //} + } + + private Task<SessionInfo> GetSession(QueryParamCollection queryString, string remoteEndpoint) + { + if (queryString == null) + { + throw new ArgumentNullException("queryString"); + } + + var token = queryString["api_key"]; + if (string.IsNullOrWhiteSpace(token)) + { + return Task.FromResult<SessionInfo>(null); + } + var deviceId = queryString["deviceId"]; + return _sessionManager.GetSessionByAuthenticationToken(token, deviceId, remoteEndpoint); + } + + public void Dispose() + { + _httpServer.WebSocketConnecting -= _httpServer_WebSocketConnecting; + _serverManager.WebSocketConnected -= _serverManager_WebSocketConnected; + } + + /// <summary> + /// Processes the message. + /// </summary> + /// <param name="message">The message.</param> + /// <returns>Task.</returns> + public Task ProcessMessage(WebSocketMessageInfo message) + { + if (string.Equals(message.MessageType, "Identity", StringComparison.OrdinalIgnoreCase)) + { + ProcessIdentityMessage(message); + } + else if (string.Equals(message.MessageType, "Context", StringComparison.OrdinalIgnoreCase)) + { + ProcessContextMessage(message); + } + else if (string.Equals(message.MessageType, "PlaybackStart", StringComparison.OrdinalIgnoreCase)) + { + OnPlaybackStart(message); + } + else if (string.Equals(message.MessageType, "PlaybackProgress", StringComparison.OrdinalIgnoreCase)) + { + OnPlaybackProgress(message); + } + else if (string.Equals(message.MessageType, "PlaybackStopped", StringComparison.OrdinalIgnoreCase)) + { + OnPlaybackStopped(message); + } + else if (string.Equals(message.MessageType, "ReportPlaybackStart", StringComparison.OrdinalIgnoreCase)) + { + ReportPlaybackStart(message); + } + else if (string.Equals(message.MessageType, "ReportPlaybackProgress", StringComparison.OrdinalIgnoreCase)) + { + ReportPlaybackProgress(message); + } + else if (string.Equals(message.MessageType, "ReportPlaybackStopped", StringComparison.OrdinalIgnoreCase)) + { + ReportPlaybackStopped(message); + } + + return _trueTaskResult; + } + + /// <summary> + /// Processes the identity message. + /// </summary> + /// <param name="message">The message.</param> + private async void ProcessIdentityMessage(WebSocketMessageInfo message) + { + _logger.Debug("Received Identity message: " + message.Data); + + var vals = message.Data.Split('|'); + + if (vals.Length < 3) + { + _logger.Error("Client sent invalid identity message."); + return; + } + + var client = vals[0]; + var deviceId = vals[1]; + var version = vals[2]; + var deviceName = vals.Length > 3 ? vals[3] : string.Empty; + + var session = _sessionManager.GetSession(deviceId, client, version); + + if (session == null && !string.IsNullOrEmpty(deviceName)) + { + _logger.Debug("Logging session activity"); + + session = await _sessionManager.LogSessionActivity(client, version, deviceId, deviceName, message.Connection.RemoteEndPoint, null).ConfigureAwait(false); + } + + if (session != null) + { + var controller = session.SessionController as WebSocketController; + + if (controller == null) + { + controller = new WebSocketController(session, _logger, _sessionManager); + } + + controller.AddWebSocket(message.Connection); + + session.SessionController = controller; + } + else + { + _logger.Warn("Unable to determine session based on identity message: {0}", message.Data); + } + } + + /// <summary> + /// Processes the context message. + /// </summary> + /// <param name="message">The message.</param> + private void ProcessContextMessage(WebSocketMessageInfo message) + { + var session = GetSessionFromMessage(message); + + if (session != null) + { + var vals = message.Data.Split('|'); + + var itemId = vals[1]; + + if (!string.IsNullOrWhiteSpace(itemId)) + { + _sessionManager.ReportNowViewingItem(session.Id, itemId); + } + } + } + + /// <summary> + /// Gets the session from message. + /// </summary> + /// <param name="message">The message.</param> + /// <returns>SessionInfo.</returns> + private SessionInfo GetSessionFromMessage(WebSocketMessageInfo message) + { + var result = _sessionManager.Sessions.FirstOrDefault(i => + { + var controller = i.SessionController as WebSocketController; + + if (controller != null) + { + if (controller.Sockets.Any(s => s.Id == message.Connection.Id)) + { + return true; + } + } + + return false; + + }); + + if (result == null) + { + _logger.Error("Unable to find session based on web socket message"); + } + + return result; + } + + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + /// <summary> + /// Reports the playback start. + /// </summary> + /// <param name="message">The message.</param> + private void OnPlaybackStart(WebSocketMessageInfo message) + { + _logger.Debug("Received PlaybackStart message"); + + var session = GetSessionFromMessage(message); + + if (session != null && session.UserId.HasValue) + { + var vals = message.Data.Split('|'); + + var itemId = vals[0]; + + var queueableMediaTypes = string.Empty; + var canSeek = true; + + if (vals.Length > 1) + { + canSeek = string.Equals(vals[1], "true", StringComparison.OrdinalIgnoreCase); + } + if (vals.Length > 2) + { + queueableMediaTypes = vals[2]; + } + + var info = new PlaybackStartInfo + { + CanSeek = canSeek, + ItemId = itemId, + SessionId = session.Id, + QueueableMediaTypes = queueableMediaTypes.Split(',').ToList() + }; + + if (vals.Length > 3) + { + info.MediaSourceId = vals[3]; + } + + if (vals.Length > 4 && !string.IsNullOrWhiteSpace(vals[4])) + { + info.AudioStreamIndex = int.Parse(vals[4], _usCulture); + } + + if (vals.Length > 5 && !string.IsNullOrWhiteSpace(vals[5])) + { + info.SubtitleStreamIndex = int.Parse(vals[5], _usCulture); + } + + _sessionManager.OnPlaybackStart(info); + } + } + + private void ReportPlaybackStart(WebSocketMessageInfo message) + { + _logger.Debug("Received ReportPlaybackStart message"); + + var session = GetSessionFromMessage(message); + + if (session != null && session.UserId.HasValue) + { + var info = _json.DeserializeFromString<PlaybackStartInfo>(message.Data); + + info.SessionId = session.Id; + + _sessionManager.OnPlaybackStart(info); + } + } + + private void ReportPlaybackProgress(WebSocketMessageInfo message) + { + //_logger.Debug("Received ReportPlaybackProgress message"); + + var session = GetSessionFromMessage(message); + + if (session != null && session.UserId.HasValue) + { + var info = _json.DeserializeFromString<PlaybackProgressInfo>(message.Data); + + info.SessionId = session.Id; + + _sessionManager.OnPlaybackProgress(info); + } + } + + /// <summary> + /// Reports the playback progress. + /// </summary> + /// <param name="message">The message.</param> + private void OnPlaybackProgress(WebSocketMessageInfo message) + { + var session = GetSessionFromMessage(message); + + if (session != null && session.UserId.HasValue) + { + var vals = message.Data.Split('|'); + + var itemId = vals[0]; + + long? positionTicks = null; + + if (vals.Length > 1) + { + long pos; + + if (long.TryParse(vals[1], out pos)) + { + positionTicks = pos; + } + } + + var isPaused = vals.Length > 2 && string.Equals(vals[2], "true", StringComparison.OrdinalIgnoreCase); + var isMuted = vals.Length > 3 && string.Equals(vals[3], "true", StringComparison.OrdinalIgnoreCase); + + var info = new PlaybackProgressInfo + { + ItemId = itemId, + PositionTicks = positionTicks, + IsMuted = isMuted, + IsPaused = isPaused, + SessionId = session.Id + }; + + if (vals.Length > 4) + { + info.MediaSourceId = vals[4]; + } + + if (vals.Length > 5 && !string.IsNullOrWhiteSpace(vals[5])) + { + info.VolumeLevel = int.Parse(vals[5], _usCulture); + } + + if (vals.Length > 5 && !string.IsNullOrWhiteSpace(vals[6])) + { + info.AudioStreamIndex = int.Parse(vals[6], _usCulture); + } + + if (vals.Length > 7 && !string.IsNullOrWhiteSpace(vals[7])) + { + info.SubtitleStreamIndex = int.Parse(vals[7], _usCulture); + } + + _sessionManager.OnPlaybackProgress(info); + } + } + + private void ReportPlaybackStopped(WebSocketMessageInfo message) + { + _logger.Debug("Received ReportPlaybackStopped message"); + + var session = GetSessionFromMessage(message); + + if (session != null && session.UserId.HasValue) + { + var info = _json.DeserializeFromString<PlaybackStopInfo>(message.Data); + + info.SessionId = session.Id; + + _sessionManager.OnPlaybackStopped(info); + } + } + + /// <summary> + /// Reports the playback stopped. + /// </summary> + /// <param name="message">The message.</param> + private void OnPlaybackStopped(WebSocketMessageInfo message) + { + _logger.Debug("Received PlaybackStopped message"); + + var session = GetSessionFromMessage(message); + + if (session != null && session.UserId.HasValue) + { + var vals = message.Data.Split('|'); + + var itemId = vals[0]; + + long? positionTicks = null; + + if (vals.Length > 1) + { + long pos; + + if (long.TryParse(vals[1], out pos)) + { + positionTicks = pos; + } + } + + var info = new PlaybackStopInfo + { + ItemId = itemId, + PositionTicks = positionTicks, + SessionId = session.Id + }; + + if (vals.Length > 2) + { + info.MediaSourceId = vals[2]; + } + + _sessionManager.OnPlaybackStopped(info); + } + } + } +} diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs new file mode 100644 index 000000000..f0ff0b5dd --- /dev/null +++ b/Emby.Server.Implementations/Session/WebSocketController.cs @@ -0,0 +1,288 @@ +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.System; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Emby.Server.Implementations.Session +{ + public class WebSocketController : ISessionController, IDisposable + { + public SessionInfo Session { get; private set; } + public IReadOnlyList<IWebSocketConnection> Sockets { get; private set; } + + private readonly ILogger _logger; + + private readonly ISessionManager _sessionManager; + + public WebSocketController(SessionInfo session, ILogger logger, ISessionManager sessionManager) + { + Session = session; + _logger = logger; + _sessionManager = sessionManager; + Sockets = new List<IWebSocketConnection>(); + } + + private bool HasOpenSockets + { + get { return GetActiveSockets().Any(); } + } + + public bool SupportsMediaControl + { + get { return HasOpenSockets; } + } + + private bool _isActive; + private DateTime _lastActivityDate; + public bool IsSessionActive + { + get + { + if (HasOpenSockets) + { + return true; + } + + //return false; + return _isActive && (DateTime.UtcNow - _lastActivityDate).TotalMinutes <= 10; + } + } + + public void OnActivity() + { + _isActive = true; + _lastActivityDate = DateTime.UtcNow; + } + + private IEnumerable<IWebSocketConnection> GetActiveSockets() + { + return Sockets + .OrderByDescending(i => i.LastActivityDate) + .Where(i => i.State == WebSocketState.Open); + } + + public void AddWebSocket(IWebSocketConnection connection) + { + var sockets = Sockets.ToList(); + sockets.Add(connection); + + Sockets = sockets; + + connection.Closed += connection_Closed; + } + + void connection_Closed(object sender, EventArgs e) + { + if (!GetActiveSockets().Any()) + { + _isActive = false; + + try + { + _sessionManager.ReportSessionEnded(Session.Id); + } + catch (Exception ex) + { + _logger.ErrorException("Error reporting session ended.", ex); + } + } + } + + private IWebSocketConnection GetActiveSocket() + { + var socket = GetActiveSockets() + .FirstOrDefault(); + + if (socket == null) + { + throw new InvalidOperationException("The requested session does not have an open web socket."); + } + + return socket; + } + + public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken) + { + return SendMessageInternal(new WebSocketMessage<PlayRequest> + { + MessageType = "Play", + Data = command + + }, cancellationToken); + } + + public Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken) + { + return SendMessageInternal(new WebSocketMessage<PlaystateRequest> + { + MessageType = "Playstate", + Data = command + + }, cancellationToken); + } + + public Task SendLibraryUpdateInfo(LibraryUpdateInfo info, CancellationToken cancellationToken) + { + return SendMessagesInternal(new WebSocketMessage<LibraryUpdateInfo> + { + MessageType = "LibraryChanged", + Data = info + + }, cancellationToken); + } + + /// <summary> + /// Sends the restart required message. + /// </summary> + /// <param name="info">The information.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public Task SendRestartRequiredNotification(SystemInfo info, CancellationToken cancellationToken) + { + return SendMessagesInternal(new WebSocketMessage<SystemInfo> + { + MessageType = "RestartRequired", + Data = info + + }, cancellationToken); + } + + + /// <summary> + /// Sends the user data change info. + /// </summary> + /// <param name="info">The info.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public Task SendUserDataChangeInfo(UserDataChangeInfo info, CancellationToken cancellationToken) + { + return SendMessagesInternal(new WebSocketMessage<UserDataChangeInfo> + { + MessageType = "UserDataChanged", + Data = info + + }, cancellationToken); + } + + /// <summary> + /// Sends the server shutdown notification. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public Task SendServerShutdownNotification(CancellationToken cancellationToken) + { + return SendMessagesInternal(new WebSocketMessage<string> + { + MessageType = "ServerShuttingDown", + Data = string.Empty + + }, cancellationToken); + } + + /// <summary> + /// Sends the server restart notification. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public Task SendServerRestartNotification(CancellationToken cancellationToken) + { + return SendMessagesInternal(new WebSocketMessage<string> + { + MessageType = "ServerRestarting", + Data = string.Empty + + }, cancellationToken); + } + + public Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken) + { + return SendMessageInternal(new WebSocketMessage<GeneralCommand> + { + MessageType = "GeneralCommand", + Data = command + + }, cancellationToken); + } + + public Task SendSessionEndedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken) + { + return SendMessagesInternal(new WebSocketMessage<SessionInfoDto> + { + MessageType = "SessionEnded", + Data = sessionInfo + + }, cancellationToken); + } + + public Task SendPlaybackStartNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken) + { + return SendMessagesInternal(new WebSocketMessage<SessionInfoDto> + { + MessageType = "PlaybackStart", + Data = sessionInfo + + }, cancellationToken); + } + + public Task SendPlaybackStoppedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken) + { + return SendMessagesInternal(new WebSocketMessage<SessionInfoDto> + { + MessageType = "PlaybackStopped", + Data = sessionInfo + + }, cancellationToken); + } + + public Task SendMessage<T>(string name, T data, CancellationToken cancellationToken) + { + return SendMessagesInternal(new WebSocketMessage<T> + { + Data = data, + MessageType = name + + }, cancellationToken); + } + + private Task SendMessageInternal<T>(WebSocketMessage<T> message, CancellationToken cancellationToken) + { + var socket = GetActiveSocket(); + + return socket.SendAsync(message, cancellationToken); + } + + private Task SendMessagesInternal<T>(WebSocketMessage<T> message, CancellationToken cancellationToken) + { + var tasks = GetActiveSockets().Select(i => Task.Run(async () => + { + try + { + await i.SendAsync(message, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error sending web socket message", ex); + } + + }, cancellationToken)); + + return Task.WhenAll(tasks); + } + + public void Dispose() + { + foreach (var socket in Sockets.ToList()) + { + socket.Closed -= connection_Closed; + } + } + } +} |
