diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs index 0c95f61120..17c36254e7 100644 --- a/MediaBrowser.Api/BaseApiService.cs +++ b/MediaBrowser.Api/BaseApiService.cs @@ -1,5 +1,7 @@ using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Session; using MediaBrowser.Model.Logging; using ServiceStack.Common.Web; using ServiceStack.ServiceHost; @@ -100,6 +102,8 @@ namespace MediaBrowser.Api /// The user manager. public IUserManager UserManager { get; set; } + public ISessionManager SessionManager { get; set; } + /// /// Gets or sets the logger. /// @@ -122,11 +126,20 @@ namespace MediaBrowser.Api { var userId = auth["UserId"]; + User user = null; + if (!string.IsNullOrEmpty(userId)) { - var user = UserManager.GetUserById(new Guid(userId)); + user = UserManager.GetUserById(new Guid(userId)); + } - UserManager.LogUserActivity(user, auth["Client"], auth["DeviceId"], auth["Device"] ?? string.Empty); + var deviceId = auth["DeviceId"]; + var device = auth["Device"]; + var client = auth["Client"]; + + if (!string.IsNullOrEmpty(client) && !string.IsNullOrEmpty(deviceId) && !string.IsNullOrEmpty(device)) + { + SessionManager.LogConnectionActivity(client, deviceId, device, user); } } } diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index b17412ee65..7ed030a873 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -88,6 +88,7 @@ + diff --git a/MediaBrowser.Api/SessionsService.cs b/MediaBrowser.Api/SessionsService.cs new file mode 100644 index 0000000000..03a352307a --- /dev/null +++ b/MediaBrowser.Api/SessionsService.cs @@ -0,0 +1,54 @@ +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Session; +using ServiceStack.ServiceHost; +using System.Collections.Generic; +using System.Linq; + +namespace MediaBrowser.Api +{ + /// + /// Class GetSessions + /// + [Route("/Sessions", "GET")] + [Api(("Gets a list of sessions"))] + public class GetSessions : IReturn> + { + /// + /// Gets or sets a value indicating whether this instance is recent. + /// + /// true if this instance is recent; otherwise, false. + public bool IsRecent { get; set; } + } + + /// + /// Class SessionsService + /// + public class SessionsService : BaseApiService + { + /// + /// The _session manager + /// + private readonly ISessionManager _sessionManager; + + /// + /// Initializes a new instance of the class. + /// + /// The session manager. + public SessionsService(ISessionManager sessionManager) + { + _sessionManager = sessionManager; + } + + /// + /// Gets the specified request. + /// + /// The request. + /// System.Object. + public object Get(GetSessions request) + { + var result = request.IsRecent ? _sessionManager.RecentConnections : _sessionManager.AllConnections; + + return ToOptimizedResult(result.ToList()); + } + } +} diff --git a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs index 5c1eff9549..53f2e9bca5 100644 --- a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs +++ b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs @@ -3,6 +3,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; using ServiceStack.ServiceHost; @@ -401,6 +402,8 @@ namespace MediaBrowser.Api.UserLibrary private readonly IItemRepository _itemRepo; + private readonly ISessionManager _sessionManager; + /// /// Initializes a new instance of the class. /// @@ -409,13 +412,14 @@ namespace MediaBrowser.Api.UserLibrary /// The user data repository. /// The item repo. /// jsonSerializer - public UserLibraryService(IUserManager userManager, ILibraryManager libraryManager, IUserDataRepository userDataRepository, IItemRepository itemRepo) + public UserLibraryService(IUserManager userManager, ILibraryManager libraryManager, IUserDataRepository userDataRepository, IItemRepository itemRepo, ISessionManager sessionManager) : base() { _userManager = userManager; _libraryManager = libraryManager; _userDataRepository = userDataRepository; _itemRepo = itemRepo; + _sessionManager = sessionManager; } /// @@ -693,7 +697,7 @@ namespace MediaBrowser.Api.UserLibrary if (auth != null) { - _userManager.OnPlaybackStart(user, item, auth["Client"], auth["DeviceId"], auth["Device"] ?? string.Empty); + _sessionManager.OnPlaybackStart(user, item, auth["Client"], auth["DeviceId"], auth["Device"] ?? string.Empty); } } @@ -711,7 +715,7 @@ namespace MediaBrowser.Api.UserLibrary if (auth != null) { - var task = _userManager.OnPlaybackProgress(user, item, request.PositionTicks, auth["Client"], auth["DeviceId"], auth["Device"] ?? string.Empty); + var task = _sessionManager.OnPlaybackProgress(user, item, request.PositionTicks, auth["Client"], auth["DeviceId"], auth["Device"] ?? string.Empty); Task.WaitAll(task); } @@ -731,7 +735,7 @@ namespace MediaBrowser.Api.UserLibrary if (auth != null) { - var task = _userManager.OnPlaybackStopped(user, item, request.PositionTicks, auth["Client"], auth["DeviceId"], auth["Device"] ?? string.Empty); + var task = _sessionManager.OnPlaybackStopped(user, item, request.PositionTicks, auth["Client"], auth["DeviceId"], auth["Device"] ?? string.Empty); Task.WaitAll(task); } diff --git a/MediaBrowser.Controller/Dto/DtoBuilder.cs b/MediaBrowser.Controller/Dto/DtoBuilder.cs index d84227059e..167ff2f78e 100644 --- a/MediaBrowser.Controller/Dto/DtoBuilder.cs +++ b/MediaBrowser.Controller/Dto/DtoBuilder.cs @@ -832,6 +832,7 @@ namespace MediaBrowser.Controller.Dto { Id = GetClientItemId(item), Name = item.Name, + MediaType = item.MediaType, Type = item.GetType().Name, IsFolder = item.IsFolder, RunTimeTicks = item.RunTimeTicks @@ -844,16 +845,6 @@ namespace MediaBrowser.Controller.Dto info.PrimaryImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Primary, imagePath); } - if (item.BackdropImagePaths != null && item.BackdropImagePaths.Count > 0) - { - imagePath = item.BackdropImagePaths[0]; - - if (!string.IsNullOrEmpty(imagePath)) - { - info.BackdropImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Backdrop, imagePath); - } - } - return info; } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index e31939d59a..43c629505d 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -683,7 +683,7 @@ namespace MediaBrowser.Controller.Entities /// Loads local trailers from the file system /// /// List{Video}. - private List LoadLocalTrailers() + private IEnumerable LoadLocalTrailers() { if (LocationType != LocationType.FileSystem) { @@ -746,7 +746,7 @@ namespace MediaBrowser.Controller.Entities /// Loads the theme songs. /// /// List{Audio.Audio}. - private List LoadThemeSongs() + private IEnumerable LoadThemeSongs() { if (LocationType != LocationType.FileSystem) { @@ -809,7 +809,7 @@ namespace MediaBrowser.Controller.Entities /// Loads the video backdrops. /// /// List{Video}. - private List + diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs new file mode 100644 index 0000000000..66facdf6de --- /dev/null +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -0,0 +1,90 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Controller.Session +{ + /// + /// Interface ISessionManager + /// + public interface ISessionManager + { + /// + /// Occurs when [playback start]. + /// + event EventHandler PlaybackStart; + + /// + /// Occurs when [playback progress]. + /// + event EventHandler PlaybackProgress; + + /// + /// Occurs when [playback stopped]. + /// + event EventHandler PlaybackStopped; + + /// + /// Gets all connections. + /// + /// All connections. + IEnumerable AllConnections { get; } + + /// + /// Gets the active connections. + /// + /// The active connections. + IEnumerable RecentConnections { get; } + + /// + /// Logs the user activity. + /// + /// Type of the client. + /// The device id. + /// Name of the device. + /// The user. + /// Task. + /// user + Task LogConnectionActivity(string clientType, string deviceId, string deviceName, User user); + + /// + /// Used to report that playback has started for an item + /// + /// The user. + /// The item. + /// Type of the client. + /// The device id. + /// Name of the device. + /// + void OnPlaybackStart(User user, BaseItem item, string clientType, string deviceId, string deviceName); + + /// + /// Used to report playback progress for an item + /// + /// The user. + /// The item. + /// The position ticks. + /// Type of the client. + /// The device id. + /// Name of the device. + /// Task. + /// + Task OnPlaybackProgress(User user, BaseItem item, long? positionTicks, string clientType, string deviceId, string deviceName); + + /// + /// Used to report that playback has ended for an item + /// + /// The user. + /// The item. + /// The position ticks. + /// Type of the client. + /// The device id. + /// Name of the device. + /// Task. + /// + Task OnPlaybackStopped(User user, BaseItem item, long? positionTicks, string clientType, string deviceId, string deviceName); + } +} \ No newline at end of file diff --git a/MediaBrowser.Model/Entities/BaseItemInfo.cs b/MediaBrowser.Model/Entities/BaseItemInfo.cs index dc7b3bebe8..0d8e35cc91 100644 --- a/MediaBrowser.Model/Entities/BaseItemInfo.cs +++ b/MediaBrowser.Model/Entities/BaseItemInfo.cs @@ -26,6 +26,12 @@ namespace MediaBrowser.Model.Entities /// The type. public string Type { get; set; } + /// + /// Gets or sets the type of the media. + /// + /// The type of the media. + public string MediaType { get; set; } + /// /// Gets or sets a value indicating whether this instance is folder. /// @@ -44,12 +50,6 @@ namespace MediaBrowser.Model.Entities /// The primary image tag. public Guid? PrimaryImageTag { get; set; } - /// - /// Gets or sets the backdrop image tag. - /// - /// The backdrop image tag. - public Guid? BackdropImageTag { get; set; } - /// /// Gets a value indicating whether this instance has primary image. /// @@ -59,15 +59,5 @@ namespace MediaBrowser.Model.Entities { get { return PrimaryImageTag.HasValue; } } - - /// - /// Gets a value indicating whether this instance has backdrop. - /// - /// true if this instance has backdrop; otherwise, false. - [IgnoreDataMember] - public bool HasBackdrop - { - get { return BackdropImageTag.HasValue; } - } } } diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 707e6ea4d4..dbfe386983 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -56,7 +56,7 @@ - + diff --git a/MediaBrowser.Model/Connectivity/ClientConnectionInfo.cs b/MediaBrowser.Model/Session/SessionInfo.cs similarity index 85% rename from MediaBrowser.Model/Connectivity/ClientConnectionInfo.cs rename to MediaBrowser.Model/Session/SessionInfo.cs index dc0c4508bc..f74db90583 100644 --- a/MediaBrowser.Model/Connectivity/ClientConnectionInfo.cs +++ b/MediaBrowser.Model/Session/SessionInfo.cs @@ -1,13 +1,19 @@ using MediaBrowser.Model.Entities; using System; -namespace MediaBrowser.Model.Connectivity +namespace MediaBrowser.Model.Session { /// - /// Class ClientConnectionInfo + /// Class SessionInfo /// - public class ClientConnectionInfo + public class SessionInfo { + /// + /// Gets or sets the id. + /// + /// The id. + public Guid Id { get; set; } + /// /// Gets or sets the user id. /// diff --git a/MediaBrowser.Server.Implementations/Library/UserManager.cs b/MediaBrowser.Server.Implementations/Library/UserManager.cs index 99485f7260..dc863ca4d9 100644 --- a/MediaBrowser.Server.Implementations/Library/UserManager.cs +++ b/MediaBrowser.Server.Implementations/Library/UserManager.cs @@ -7,7 +7,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Connectivity; using MediaBrowser.Model.Logging; using System; using System.Collections.Concurrent; @@ -17,6 +16,7 @@ using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Model.Session; namespace MediaBrowser.Server.Implementations.Library { @@ -28,8 +28,8 @@ namespace MediaBrowser.Server.Implementations.Library /// /// The _active connections /// - private readonly ConcurrentDictionary _activeConnections = - new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary _activeConnections = + new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); /// /// The _users @@ -70,7 +70,7 @@ namespace MediaBrowser.Server.Implementations.Library /// Gets all connections. /// /// All connections. - public IEnumerable AllConnections + public IEnumerable AllConnections { get { return _activeConnections.Values.OrderByDescending(c => c.LastActivityDate); } } @@ -79,7 +79,7 @@ namespace MediaBrowser.Server.Implementations.Library /// Gets the active connections. /// /// The active connections. - public IEnumerable RecentConnections + public IEnumerable RecentConnections { get { return AllConnections.Where(c => (DateTime.UtcNow - c.LastActivityDate).TotalMinutes <= 5); } } @@ -89,8 +89,6 @@ namespace MediaBrowser.Server.Implementations.Library /// private readonly ILogger _logger; - private readonly IUserDataRepository _userDataRepository; - /// /// Gets or sets the configuration manager. /// @@ -109,11 +107,10 @@ namespace MediaBrowser.Server.Implementations.Library /// The logger. /// The configuration manager. /// The user data repository. - public UserManager(ILogger logger, IServerConfigurationManager configurationManager, IUserDataRepository userDataRepository) + public UserManager(ILogger logger, IServerConfigurationManager configurationManager) { _logger = logger; ConfigurationManager = configurationManager; - _userDataRepository = userDataRepository; } #region Events @@ -222,116 +219,6 @@ namespace MediaBrowser.Server.Implementations.Library } } - /// - /// Logs the user activity. - /// - /// The user. - /// Type of the client. - /// The device id. - /// Name of the device. - /// Task. - /// user - public Task LogUserActivity(User user, string clientType, string deviceId, string deviceName) - { - if (user == null) - { - throw new ArgumentNullException("user"); - } - - var activityDate = DateTime.UtcNow; - - var lastActivityDate = user.LastActivityDate; - - user.LastActivityDate = activityDate; - - LogConnection(user.Id, clientType, deviceId, deviceName, activityDate); - - // Don't log in the db anymore frequently than 10 seconds - if (lastActivityDate.HasValue && (activityDate - lastActivityDate.Value).TotalSeconds < 10) - { - return Task.FromResult(true); - } - - // Save this directly. No need to fire off all the events for this. - return UserRepository.SaveUser(user, CancellationToken.None); - } - - /// - /// Updates the now playing item id. - /// - /// The user. - /// Type of the client. - /// The device id. - /// Name of the device. - /// The item. - /// The current position ticks. - private void UpdateNowPlayingItemId(User user, string clientType, string deviceId, string deviceName, BaseItem item, long? currentPositionTicks = null) - { - var conn = GetConnection(user.Id, clientType, deviceId, deviceName); - - conn.NowPlayingPositionTicks = currentPositionTicks; - conn.NowPlayingItem = DtoBuilder.GetBaseItemInfo(item); - conn.LastActivityDate = DateTime.UtcNow; - } - - /// - /// Removes the now playing item id. - /// - /// The user. - /// Type of the client. - /// The device id. - /// Name of the device. - /// The item. - private void RemoveNowPlayingItemId(User user, string clientType, string deviceId, string deviceName, BaseItem item) - { - var conn = GetConnection(user.Id, clientType, deviceId, deviceName); - - if (conn.NowPlayingItem != null && conn.NowPlayingItem.Id.Equals(item.Id.ToString())) - { - conn.NowPlayingItem = null; - conn.NowPlayingPositionTicks = null; - } - } - - /// - /// Logs the connection. - /// - /// The user id. - /// Type of the client. - /// The device id. - /// Name of the device. - /// The last activity date. - private void LogConnection(Guid userId, string clientType, string deviceId, string deviceName, DateTime lastActivityDate) - { - GetConnection(userId, clientType, deviceId, deviceName).LastActivityDate = lastActivityDate; - } - - /// - /// Gets the connection. - /// - /// The user id. - /// Type of the client. - /// The device id. - /// Name of the device. - /// ClientConnectionInfo. - private ClientConnectionInfo GetConnection(Guid userId, string clientType, string deviceId, string deviceName) - { - var key = clientType + deviceId; - - var connection = _activeConnections.GetOrAdd(key, keyName => new ClientConnectionInfo - { - UserId = userId.ToString(), - Client = clientType, - DeviceName = deviceName, - DeviceId = deviceId - }); - - connection.DeviceName = deviceName; - connection.UserId = userId.ToString(); - - return connection; - } - /// /// Loads the users from the repository /// @@ -560,182 +447,5 @@ namespace MediaBrowser.Server.Implementations.Library DateModified = DateTime.UtcNow }; } - - /// - /// Used to report that playback has started for an item - /// - /// The user. - /// The item. - /// Type of the client. - /// The device id. - /// Name of the device. - /// - public void OnPlaybackStart(User user, BaseItem item, string clientType, string deviceId, string deviceName) - { - if (user == null) - { - throw new ArgumentNullException(); - } - if (item == null) - { - throw new ArgumentNullException(); - } - - UpdateNowPlayingItemId(user, clientType, deviceId, deviceName, item); - - // Nothing to save here - // Fire events to inform plugins - EventHelper.QueueEventIfNotNull(PlaybackStart, this, new PlaybackProgressEventArgs - { - Item = item, - User = user - }, _logger); - } - - /// - /// Used to report playback progress for an item - /// - /// The user. - /// The item. - /// The position ticks. - /// Type of the client. - /// The device id. - /// Name of the device. - /// Task. - /// - public async Task OnPlaybackProgress(User user, BaseItem item, long? positionTicks, string clientType, string deviceId, string deviceName) - { - if (user == null) - { - throw new ArgumentNullException(); - } - if (item == null) - { - throw new ArgumentNullException(); - } - - UpdateNowPlayingItemId(user, clientType, deviceId, deviceName, item, positionTicks); - - var key = item.GetUserDataKey(); - - if (positionTicks.HasValue) - { - var data = await _userDataRepository.GetUserData(user.Id, key).ConfigureAwait(false); - - UpdatePlayState(item, data, positionTicks.Value, false); - await _userDataRepository.SaveUserData(user.Id, key, data, CancellationToken.None).ConfigureAwait(false); - } - - EventHelper.QueueEventIfNotNull(PlaybackProgress, this, new PlaybackProgressEventArgs - { - Item = item, - User = user, - PlaybackPositionTicks = positionTicks - }, _logger); - } - - /// - /// Used to report that playback has ended for an item - /// - /// The user. - /// The item. - /// The position ticks. - /// Type of the client. - /// The device id. - /// Name of the device. - /// Task. - /// - public async Task OnPlaybackStopped(User user, BaseItem item, long? positionTicks, string clientType, string deviceId, string deviceName) - { - if (user == null) - { - throw new ArgumentNullException(); - } - if (item == null) - { - throw new ArgumentNullException(); - } - - RemoveNowPlayingItemId(user, clientType, deviceId, deviceName, item); - - var key = item.GetUserDataKey(); - - var data = await _userDataRepository.GetUserData(user.Id, key).ConfigureAwait(false); - - if (positionTicks.HasValue) - { - UpdatePlayState(item, data, positionTicks.Value, true); - } - else - { - // If the client isn't able to report this, then we'll just have to make an assumption - data.PlayCount++; - data.Played = true; - } - - await _userDataRepository.SaveUserData(user.Id, key, data, CancellationToken.None).ConfigureAwait(false); - - EventHelper.QueueEventIfNotNull(PlaybackStopped, this, new PlaybackProgressEventArgs - { - Item = item, - User = user, - PlaybackPositionTicks = positionTicks - }, _logger); - } - - /// - /// Updates playstate position for an item but does not save - /// - /// The item - /// User data for the item - /// The current playback position - /// Whether or not to increment playcount - private void UpdatePlayState(BaseItem item, UserItemData data, long positionTicks, bool incrementPlayCount) - { - // If a position has been reported, and if we know the duration - if (positionTicks > 0 && item.RunTimeTicks.HasValue && item.RunTimeTicks > 0) - { - var pctIn = Decimal.Divide(positionTicks, item.RunTimeTicks.Value) * 100; - - // Don't track in very beginning - if (pctIn < ConfigurationManager.Configuration.MinResumePct) - { - positionTicks = 0; - incrementPlayCount = false; - } - - // If we're at the end, assume completed - else if (pctIn > ConfigurationManager.Configuration.MaxResumePct || positionTicks >= item.RunTimeTicks.Value) - { - positionTicks = 0; - data.Played = true; - } - - else - { - // Enforce MinResumeDuration - var durationSeconds = TimeSpan.FromTicks(item.RunTimeTicks.Value).TotalSeconds; - - if (durationSeconds < ConfigurationManager.Configuration.MinResumeDurationSeconds) - { - positionTicks = 0; - data.Played = true; - } - } - } - - if (item is Audio) - { - data.PlaybackPositionTicks = 0; - } - - data.PlaybackPositionTicks = positionTicks; - - if (incrementPlayCount) - { - data.PlayCount++; - data.LastPlayedDate = DateTime.UtcNow; - } - } } } diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index f9e7f0385b..45515b81f5 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -156,6 +156,10 @@ + + Code + + diff --git a/MediaBrowser.Server.Implementations/Session/SessionManager.cs b/MediaBrowser.Server.Implementations/Session/SessionManager.cs new file mode 100644 index 0000000000..051c8fb688 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Session/SessionManager.cs @@ -0,0 +1,372 @@ +using MediaBrowser.Common.Events; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Session; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.Session +{ + public class SessionManager : ISessionManager + { + private readonly IUserDataRepository _userDataRepository; + + private readonly IUserRepository _userRepository; + + /// + /// The _logger + /// + private readonly ILogger _logger; + + /// + /// Gets or sets the configuration manager. + /// + /// The configuration manager. + private readonly IServerConfigurationManager _configurationManager; + + /// + /// The _active connections + /// + private readonly ConcurrentDictionary _activeConnections = + new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + private readonly ConcurrentDictionary _websocketConnections = + new ConcurrentDictionary(); + + /// + /// 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 SessionManager(IUserDataRepository userDataRepository, IServerConfigurationManager configurationManager, ILogger logger, IUserRepository userRepository) + { + _userDataRepository = userDataRepository; + _configurationManager = configurationManager; + _logger = logger; + _userRepository = userRepository; + } + + /// + /// Gets all connections. + /// + /// All connections. + public IEnumerable AllConnections + { + get { return _activeConnections.Values.OrderByDescending(c => c.LastActivityDate); } + } + + /// + /// Gets the active connections. + /// + /// The active connections. + public IEnumerable RecentConnections + { + get { return AllConnections.Where(c => (DateTime.UtcNow - c.LastActivityDate).TotalMinutes <= 5); } + } + + private readonly Task _trueTaskResult = Task.FromResult(true); + + /// + /// Logs the user activity. + /// + /// Type of the client. + /// The device id. + /// Name of the device. + /// The user. + /// Task. + /// user + public Task LogConnectionActivity(string clientType, string deviceId, string deviceName, User user) + { + var activityDate = DateTime.UtcNow; + + GetConnection(clientType, deviceId, deviceName, user).LastActivityDate = activityDate; + + if (user == null) + { + return _trueTaskResult; + } + + 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 _trueTaskResult; + } + + // Save this directly. No need to fire off all the events for this. + return _userRepository.SaveUser(user, CancellationToken.None); + } + + /// + /// Updates the now playing item id. + /// + /// The user. + /// Type of the client. + /// The device id. + /// Name of the device. + /// The item. + /// The current position ticks. + private void UpdateNowPlayingItemId(User user, string clientType, string deviceId, string deviceName, BaseItem item, long? currentPositionTicks = null) + { + var conn = GetConnection(clientType, deviceId, deviceName, user); + + conn.NowPlayingPositionTicks = currentPositionTicks; + conn.NowPlayingItem = DtoBuilder.GetBaseItemInfo(item); + conn.LastActivityDate = DateTime.UtcNow; + } + + /// + /// Removes the now playing item id. + /// + /// The user. + /// Type of the client. + /// The device id. + /// Name of the device. + /// The item. + private void RemoveNowPlayingItemId(User user, string clientType, string deviceId, string deviceName, BaseItem item) + { + var conn = GetConnection(clientType, deviceId, deviceName, user); + + if (conn.NowPlayingItem != null && conn.NowPlayingItem.Id.Equals(item.Id.ToString())) + { + conn.NowPlayingItem = null; + conn.NowPlayingPositionTicks = null; + } + } + + /// + /// Gets the connection. + /// + /// Type of the client. + /// The device id. + /// Name of the device. + /// The user. + /// SessionInfo. + private SessionInfo GetConnection(string clientType, string deviceId, string deviceName, User user) + { + var key = clientType + deviceId; + + var connection = _activeConnections.GetOrAdd(key, keyName => new SessionInfo + { + Client = clientType, + DeviceId = deviceId, + Id = Guid.NewGuid() + }); + + connection.DeviceName = deviceName; + + connection.UserId = user == null ? null : user.Id.ToString(); + + return connection; + } + + /// + /// Used to report that playback has started for an item + /// + /// The user. + /// The item. + /// Type of the client. + /// The device id. + /// Name of the device. + /// + public void OnPlaybackStart(User user, BaseItem item, string clientType, string deviceId, string deviceName) + { + if (user == null) + { + throw new ArgumentNullException(); + } + if (item == null) + { + throw new ArgumentNullException(); + } + + UpdateNowPlayingItemId(user, clientType, deviceId, deviceName, item); + + // Nothing to save here + // Fire events to inform plugins + EventHelper.QueueEventIfNotNull(PlaybackStart, this, new PlaybackProgressEventArgs + { + Item = item, + User = user + }, _logger); + } + + /// + /// Used to report playback progress for an item + /// + /// The user. + /// The item. + /// The position ticks. + /// Type of the client. + /// The device id. + /// Name of the device. + /// Task. + /// + public async Task OnPlaybackProgress(User user, BaseItem item, long? positionTicks, string clientType, string deviceId, string deviceName) + { + if (user == null) + { + throw new ArgumentNullException(); + } + if (item == null) + { + throw new ArgumentNullException(); + } + + UpdateNowPlayingItemId(user, clientType, deviceId, deviceName, item, positionTicks); + + var key = item.GetUserDataKey(); + + if (positionTicks.HasValue) + { + var data = await _userDataRepository.GetUserData(user.Id, key).ConfigureAwait(false); + + UpdatePlayState(item, data, positionTicks.Value, false); + await _userDataRepository.SaveUserData(user.Id, key, data, CancellationToken.None).ConfigureAwait(false); + } + + EventHelper.QueueEventIfNotNull(PlaybackProgress, this, new PlaybackProgressEventArgs + { + Item = item, + User = user, + PlaybackPositionTicks = positionTicks + }, _logger); + } + + /// + /// Used to report that playback has ended for an item + /// + /// The user. + /// The item. + /// The position ticks. + /// Type of the client. + /// The device id. + /// Name of the device. + /// Task. + /// + public async Task OnPlaybackStopped(User user, BaseItem item, long? positionTicks, string clientType, string deviceId, string deviceName) + { + if (user == null) + { + throw new ArgumentNullException(); + } + if (item == null) + { + throw new ArgumentNullException(); + } + + RemoveNowPlayingItemId(user, clientType, deviceId, deviceName, item); + + var key = item.GetUserDataKey(); + + var data = await _userDataRepository.GetUserData(user.Id, key).ConfigureAwait(false); + + if (positionTicks.HasValue) + { + UpdatePlayState(item, data, positionTicks.Value, true); + } + else + { + // If the client isn't able to report this, then we'll just have to make an assumption + data.PlayCount++; + data.Played = true; + } + + await _userDataRepository.SaveUserData(user.Id, key, data, CancellationToken.None).ConfigureAwait(false); + + EventHelper.QueueEventIfNotNull(PlaybackStopped, this, new PlaybackProgressEventArgs + { + Item = item, + User = user, + PlaybackPositionTicks = positionTicks + }, _logger); + } + + /// + /// Updates playstate position for an item but does not save + /// + /// The item + /// User data for the item + /// The current playback position + /// Whether or not to increment playcount + private void UpdatePlayState(BaseItem item, UserItemData data, long positionTicks, bool incrementPlayCount) + { + // If a position has been reported, and if we know the duration + if (positionTicks > 0 && item.RunTimeTicks.HasValue && item.RunTimeTicks > 0) + { + var pctIn = Decimal.Divide(positionTicks, item.RunTimeTicks.Value) * 100; + + // Don't track in very beginning + if (pctIn < _configurationManager.Configuration.MinResumePct) + { + positionTicks = 0; + incrementPlayCount = false; + } + + // If we're at the end, assume completed + else if (pctIn > _configurationManager.Configuration.MaxResumePct || positionTicks >= item.RunTimeTicks.Value) + { + positionTicks = 0; + data.Played = true; + } + + else + { + // Enforce MinResumeDuration + var durationSeconds = TimeSpan.FromTicks(item.RunTimeTicks.Value).TotalSeconds; + + if (durationSeconds < _configurationManager.Configuration.MinResumeDurationSeconds) + { + positionTicks = 0; + data.Played = true; + } + } + } + + if (item is Audio) + { + data.PlaybackPositionTicks = 0; + } + + data.PlaybackPositionTicks = positionTicks; + + if (incrementPlayCount) + { + data.PlayCount++; + data.LastPlayedDate = DateTime.UtcNow; + } + } + + /// + /// Identifies the web socket. + /// + /// The session id. + /// The web socket. + public void IdentifyWebSocket(Guid sessionId, IWebSocketConnection webSocket) + { + _websocketConnections.AddOrUpdate(sessionId, webSocket, (key, existing) => webSocket); + } + } +} diff --git a/MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs b/MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs new file mode 100644 index 0000000000..7ce074cd7f --- /dev/null +++ b/MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs @@ -0,0 +1,58 @@ +using System.Linq; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Session; +using System; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.Session +{ + /// + /// Class SessionWebSocketListener + /// + public class SessionWebSocketListener : IWebSocketListener + { + /// + /// The _true task result + /// + private readonly Task _trueTaskResult = Task.FromResult(true); + + /// + /// The _session manager + /// + private readonly ISessionManager _sessionManager; + + /// + /// Initializes a new instance of the class. + /// + /// The session manager. + public SessionWebSocketListener(ISessionManager sessionManager) + { + _sessionManager = sessionManager; + } + + /// + /// Processes the message. + /// + /// The message. + /// Task. + public Task ProcessMessage(WebSocketMessageInfo message) + { + if (string.Equals(message.MessageType, "Identify", StringComparison.OrdinalIgnoreCase)) + { + var vals = message.Data.Split('|'); + + var deviceId = vals[0]; + var client = vals[1]; + + var session = _sessionManager.AllConnections.FirstOrDefault(i => string.Equals(i.DeviceId, deviceId) && string.Equals(i.Client, client)); + + if (session != null) + { + ((SessionManager)_sessionManager).IdentifyWebSocket(session.Id, message.Connection); + } + } + + return _trueTaskResult; + } + } +} diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index 37e6d1b8d7..a35ac44ea3 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -20,6 +20,7 @@ using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; +using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Updates; using MediaBrowser.Controller.Weather; @@ -37,6 +38,7 @@ using MediaBrowser.Server.Implementations.Library; using MediaBrowser.Server.Implementations.MediaEncoder; using MediaBrowser.Server.Implementations.Providers; using MediaBrowser.Server.Implementations.ServerManager; +using MediaBrowser.Server.Implementations.Session; using MediaBrowser.Server.Implementations.Sqlite; using MediaBrowser.Server.Implementations.Udp; using MediaBrowser.Server.Implementations.Updates; @@ -251,7 +253,7 @@ namespace MediaBrowser.ServerApplication ItemRepository = new SQLiteItemRepository(ApplicationPaths, JsonSerializer, LogManager); RegisterSingleInstance(ItemRepository); - UserManager = new UserManager(Logger, ServerConfigurationManager, UserDataRepository); + UserManager = new UserManager(Logger, ServerConfigurationManager); RegisterSingleInstance(UserManager); LibraryManager = new LibraryManager(Logger, TaskManager, UserManager, ServerConfigurationManager, UserDataRepository); @@ -274,6 +276,9 @@ namespace MediaBrowser.ServerApplication MediaEncoder = new MediaEncoder(LogManager.GetLogger("MediaEncoder"), ZipClient, ApplicationPaths, JsonSerializer); RegisterSingleInstance(MediaEncoder); + var clientConnectionManager = new SessionManager(UserDataRepository, ServerConfigurationManager, Logger, UserRepository); + RegisterSingleInstance(clientConnectionManager); + HttpServer = await _httpServerCreationTask.ConfigureAwait(false); RegisterSingleInstance(HttpServer, false); diff --git a/MediaBrowser.WebDashboard/Api/DashboardInfo.cs b/MediaBrowser.WebDashboard/Api/DashboardInfo.cs index 31f0f47284..500464ed8f 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardInfo.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardInfo.cs @@ -1,5 +1,5 @@ -using MediaBrowser.Model.Connectivity; -using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Session; using MediaBrowser.Model.System; using MediaBrowser.Model.Tasks; using System; @@ -33,7 +33,7 @@ namespace MediaBrowser.WebDashboard.Api /// Gets or sets the active connections. /// /// The active connections. - public ClientConnectionInfo[] ActiveConnections { get; set; } + public SessionInfo[] ActiveConnections { get; set; } /// /// Gets or sets the users. diff --git a/MediaBrowser.WebDashboard/Api/DashboardInfoWebSocketListener.cs b/MediaBrowser.WebDashboard/Api/DashboardInfoWebSocketListener.cs index 1c40371798..299ba46825 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardInfoWebSocketListener.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardInfoWebSocketListener.cs @@ -2,6 +2,7 @@ using MediaBrowser.Common.ScheduledTasks; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Session; using MediaBrowser.Model.Logging; using System.ComponentModel.Composition; using System.Threading.Tasks; @@ -36,6 +37,7 @@ namespace MediaBrowser.WebDashboard.Api /// private readonly IUserManager _userManager; private readonly ILibraryManager _libraryManager; + private readonly ISessionManager _sessionManager; /// /// Initializes a new instance of the class. @@ -45,13 +47,14 @@ namespace MediaBrowser.WebDashboard.Api /// The task manager. /// The user manager. /// The library manager. - public DashboardInfoWebSocketListener(IServerApplicationHost appHost, ILogger logger, ITaskManager taskManager, IUserManager userManager, ILibraryManager libraryManager) + public DashboardInfoWebSocketListener(IServerApplicationHost appHost, ILogger logger, ITaskManager taskManager, IUserManager userManager, ILibraryManager libraryManager, ISessionManager sessionManager) : base(logger) { _appHost = appHost; _taskManager = taskManager; _userManager = userManager; _libraryManager = libraryManager; + _sessionManager = sessionManager; } /// @@ -61,7 +64,7 @@ namespace MediaBrowser.WebDashboard.Api /// Task{IEnumerable{TaskInfo}}. protected override Task GetDataToSend(object state) { - return DashboardService.GetDashboardInfo(_appHost, Logger, _taskManager, _userManager, _libraryManager); + return DashboardService.GetDashboardInfo(_appHost, Logger, _taskManager, _userManager, _libraryManager, _sessionManager); } } } diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index ab03ca3580..b33c152f1d 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -7,6 +7,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Session; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Tasks; using ServiceStack.ServiceHost; @@ -127,6 +128,8 @@ namespace MediaBrowser.WebDashboard.Api /// private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly ISessionManager _sessionManager; + /// /// Initializes a new instance of the class. /// @@ -135,13 +138,14 @@ namespace MediaBrowser.WebDashboard.Api /// The app host. /// The library manager. /// The server configuration manager. - public DashboardService(ITaskManager taskManager, IUserManager userManager, IServerApplicationHost appHost, ILibraryManager libraryManager, IServerConfigurationManager serverConfigurationManager) + public DashboardService(ITaskManager taskManager, IUserManager userManager, IServerApplicationHost appHost, ILibraryManager libraryManager, IServerConfigurationManager serverConfigurationManager, ISessionManager sessionManager) { _taskManager = taskManager; _userManager = userManager; _appHost = appHost; _libraryManager = libraryManager; _serverConfigurationManager = serverConfigurationManager; + _sessionManager = sessionManager; } /// @@ -180,7 +184,7 @@ namespace MediaBrowser.WebDashboard.Api /// System.Object. public object Get(GetDashboardInfo request) { - var result = GetDashboardInfo(_appHost, Logger, _taskManager, _userManager, _libraryManager).Result; + var result = GetDashboardInfo(_appHost, Logger, _taskManager, _userManager, _libraryManager, _sessionManager).Result; return ResultFactory.GetOptimizedResult(RequestContext, result); } @@ -193,10 +197,16 @@ namespace MediaBrowser.WebDashboard.Api /// The task manager. /// The user manager. /// The library manager. + /// The connection manager. /// DashboardInfo. - public static async Task GetDashboardInfo(IServerApplicationHost appHost, ILogger logger, ITaskManager taskManager, IUserManager userManager, ILibraryManager libraryManager) + public static async Task GetDashboardInfo(IServerApplicationHost appHost, + ILogger logger, + ITaskManager taskManager, + IUserManager userManager, + ILibraryManager libraryManager, + ISessionManager connectionManager) { - var connections = userManager.RecentConnections.ToArray(); + var connections = connectionManager.RecentConnections.ToArray(); var dtoBuilder = new UserDtoBuilder(logger); diff --git a/MediaBrowser.WebDashboard/ApiClient.js b/MediaBrowser.WebDashboard/ApiClient.js index ae97e86f11..dead882b91 100644 --- a/MediaBrowser.WebDashboard/ApiClient.js +++ b/MediaBrowser.WebDashboard/ApiClient.js @@ -159,6 +159,9 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) { webSocket.onopen = function () { setTimeout(function () { + + self.sendWebSocketMessage("Identify", deviceId + "|" + clientName); + $(self).trigger("websocketopen"); }, 500); }; diff --git a/MediaBrowser.WebDashboard/packages.config b/MediaBrowser.WebDashboard/packages.config index d721e1dc7d..01c9cc1086 100644 --- a/MediaBrowser.WebDashboard/packages.config +++ b/MediaBrowser.WebDashboard/packages.config @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/MediaBrowser.sln b/MediaBrowser.sln index f9f5e94365..eb3251f741 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -173,4 +173,7 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(Performance) = preSolution + HasPerformanceSessions = true + EndGlobalSection EndGlobal