From 16b9d26ab5e52c3c72dd24f17587ca4775ff79dd Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Thu, 3 Oct 2013 14:02:23 -0400 Subject: [PATCH] fixes #273 - Marking/unmarking Favorite status doesn't cause a library changed notification --- .../Session/ISessionController.cs | 24 ++- .../Session/ISessionManager.cs | 16 +- .../MediaBrowser.Model.Portable.csproj | 3 + .../MediaBrowser.Model.net35.csproj | 3 + MediaBrowser.Model/Dto/UserItemDataDto.cs | 6 + MediaBrowser.Model/MediaBrowser.Model.csproj | 1 + .../Session/UserDataChangeInfo.cs | 23 +++ .../Dto/DtoService.cs | 3 +- ...SocketEvents.cs => ServerEventNotifier.cs} | 10 +- .../EntryPoints/UserDataChangeNotifier.cs | 140 ++++++++++++++++++ ...MediaBrowser.Server.Implementations.csproj | 3 +- .../Session/SessionManager.cs | 62 +++++++- .../Session/SessionWebSocketListener.cs | 13 +- .../Session/WebSocketController.cs | 55 ++++++- .../ApplicationHost.cs | 8 +- MediaBrowser.WebDashboard/ApiClient.js | 2 +- MediaBrowser.WebDashboard/packages.config | 2 +- 17 files changed, 352 insertions(+), 22 deletions(-) create mode 100644 MediaBrowser.Model/Session/UserDataChangeInfo.cs rename MediaBrowser.Server.Implementations/EntryPoints/{WebSocketEvents.cs => ServerEventNotifier.cs} (92%) create mode 100644 MediaBrowser.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs index 597a14cc60..21206af757 100644 --- a/MediaBrowser.Controller/Session/ISessionController.cs +++ b/MediaBrowser.Controller/Session/ISessionController.cs @@ -72,6 +72,28 @@ namespace MediaBrowser.Controller.Session /// /// The cancellation token. /// Task. - Task SendRestartRequiredMessage(CancellationToken cancellationToken); + Task SendRestartRequiredNotification(CancellationToken cancellationToken); + + /// + /// Sends the user data change info. + /// + /// The info. + /// The cancellation token. + /// Task. + Task SendUserDataChangeInfo(UserDataChangeInfo info, CancellationToken cancellationToken); + + /// + /// Sends the server shutdown notification. + /// + /// The cancellation token. + /// Task. + Task SendServerShutdownNotification(CancellationToken cancellationToken); + + /// + /// Sends the server restart notification. + /// + /// The cancellation token. + /// Task. + Task SendServerRestartNotification(CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index b2d111e546..771d8f72e5 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -119,6 +119,20 @@ namespace MediaBrowser.Controller.Session /// /// The cancellation token. /// Task. - Task SendRestartRequiredMessage(CancellationToken cancellationToken); + Task SendRestartRequiredNotification(CancellationToken cancellationToken); + + /// + /// Sends the server shutdown notification. + /// + /// The cancellation token. + /// Task. + Task SendServerShutdownNotification(CancellationToken cancellationToken); + + /// + /// Sends the server restart notification. + /// + /// The cancellation token. + /// Task. + Task SendServerRestartNotification(CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj index 60eacd91ce..907433a809 100644 --- a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj +++ b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj @@ -350,6 +350,9 @@ Session\SystemCommand.cs + + Session\UserDataChangeInfo.cs + System\SystemInfo.cs diff --git a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj index 70cd4533dc..076778d33b 100644 --- a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj +++ b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj @@ -334,6 +334,9 @@ Session\SystemCommand.cs + + Session\UserDataChangeInfo.cs + System\SystemInfo.cs diff --git a/MediaBrowser.Model/Dto/UserItemDataDto.cs b/MediaBrowser.Model/Dto/UserItemDataDto.cs index e1b534eb12..8e8e7adbe2 100644 --- a/MediaBrowser.Model/Dto/UserItemDataDto.cs +++ b/MediaBrowser.Model/Dto/UserItemDataDto.cs @@ -50,6 +50,12 @@ namespace MediaBrowser.Model.Dto /// true if played; otherwise, false. public bool Played { get; set; } + /// + /// Gets or sets the key. + /// + /// The key. + public string Key { get; set; } + public event PropertyChangedEventHandler PropertyChanged; } } diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index e83df53548..ccb6221112 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -126,6 +126,7 @@ + diff --git a/MediaBrowser.Model/Session/UserDataChangeInfo.cs b/MediaBrowser.Model/Session/UserDataChangeInfo.cs new file mode 100644 index 0000000000..f92f445860 --- /dev/null +++ b/MediaBrowser.Model/Session/UserDataChangeInfo.cs @@ -0,0 +1,23 @@ +using MediaBrowser.Model.Dto; +using System.Collections.Generic; + +namespace MediaBrowser.Model.Session +{ + /// + /// Class UserDataChangeInfo + /// + public class UserDataChangeInfo + { + /// + /// Gets or sets the user id. + /// + /// The user id. + public string UserId { get; set; } + + /// + /// Gets or sets the user data list. + /// + /// The user data list. + public List UserDataList { get; set; } + } +} diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/MediaBrowser.Server.Implementations/Dto/DtoService.cs index 00808ad330..3c3e01151e 100644 --- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs +++ b/MediaBrowser.Server.Implementations/Dto/DtoService.cs @@ -340,7 +340,8 @@ namespace MediaBrowser.Server.Implementations.Dto PlayCount = data.PlayCount, Rating = data.Rating, Played = data.Played, - LastPlayedDate = data.LastPlayedDate + LastPlayedDate = data.LastPlayedDate, + Key = data.Key }; } private void SetBookProperties(BaseItemDto dto, Book item) diff --git a/MediaBrowser.Server.Implementations/EntryPoints/WebSocketEvents.cs b/MediaBrowser.Server.Implementations/EntryPoints/ServerEventNotifier.cs similarity index 92% rename from MediaBrowser.Server.Implementations/EntryPoints/WebSocketEvents.cs rename to MediaBrowser.Server.Implementations/EntryPoints/ServerEventNotifier.cs index 4349b6976a..0925ca86c4 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/WebSocketEvents.cs +++ b/MediaBrowser.Server.Implementations/EntryPoints/ServerEventNotifier.cs @@ -18,7 +18,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints /// /// Class WebSocketEvents /// - public class WebSocketEvents : IServerEntryPoint + public class ServerEventNotifier : IServerEntryPoint { /// /// The _server manager @@ -47,15 +47,15 @@ namespace MediaBrowser.Server.Implementations.EntryPoints private readonly IDtoService _dtoService; - private ISessionManager _sessionManager; + private readonly ISessionManager _sessionManager; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The server manager. /// The logger. /// The user manager. - public WebSocketEvents(IServerManager serverManager, IServerApplicationHost appHost, IUserManager userManager, IInstallationManager installationManager, ITaskManager taskManager, IDtoService dtoService, ISessionManager sessionManager) + public ServerEventNotifier(IServerManager serverManager, IServerApplicationHost appHost, IUserManager userManager, IInstallationManager installationManager, ITaskManager taskManager, IDtoService dtoService, ISessionManager sessionManager) { _serverManager = serverManager; _userManager = userManager; @@ -131,7 +131,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints /// The instance containing the event data. void kernel_HasPendingRestartChanged(object sender, EventArgs e) { - _sessionManager.SendRestartRequiredMessage(CancellationToken.None); + _sessionManager.SendRestartRequiredNotification(CancellationToken.None); } /// diff --git a/MediaBrowser.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/MediaBrowser.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs new file mode 100644 index 0000000000..fc35e040df --- /dev/null +++ b/MediaBrowser.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs @@ -0,0 +1,140 @@ +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Session; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.EntryPoints +{ + class UserDataChangeNotifier : IServerEntryPoint + { + private readonly ISessionManager _sessionManager; + private readonly ILogger _logger; + private readonly IDtoService _dtoService; + private readonly IUserDataManager _userDataManager; + + private readonly object _syncLock = new object(); + private Timer UpdateTimer { get; set; } + private const int UpdateDuration = 2000; + + private readonly Dictionary> _changedKeys = new Dictionary>(); + + public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IDtoService dtoService, ILogger logger) + { + _userDataManager = userDataManager; + _sessionManager = sessionManager; + _dtoService = dtoService; + _logger = logger; + } + + public void Run() + { + _userDataManager.UserDataSaved += _userDataManager_UserDataSaved; + } + + void _userDataManager_UserDataSaved(object sender, UserDataSaveEventArgs e) + { + if (e.SaveReason == UserDataSaveReason.PlaybackProgress) + { + return; + } + + lock (_syncLock) + { + if (UpdateTimer == null) + { + UpdateTimer = new Timer(UpdateTimerCallback, null, UpdateDuration, + Timeout.Infinite); + } + else + { + UpdateTimer.Change(UpdateDuration, Timeout.Infinite); + } + + List keys; + + if (!_changedKeys.TryGetValue(e.UserId, out keys)) + { + keys = new List(); + _changedKeys[e.UserId] = keys; + } + + keys.Add(e.Key); + } + } + + private void UpdateTimerCallback(object state) + { + lock (_syncLock) + { + // Remove dupes in case some were saved multiple times + var changes = _changedKeys.ToList(); + _changedKeys.Clear(); + + SendNotifications(changes, CancellationToken.None); + + if (UpdateTimer != null) + { + UpdateTimer.Dispose(); + UpdateTimer = null; + } + } + } + + private async Task SendNotifications(List>> changes, CancellationToken cancellationToken) + { + foreach (var pair in changes) + { + var userId = pair.Key; + var userSessions = _sessionManager.Sessions + .Where(u => u.User != null && u.User.Id == userId && u.SessionController != null && u.IsActive) + .ToList(); + + if (userSessions.Count > 0) + { + var dtoList = pair.Value + .Select(i => _dtoService.GetUserItemDataDto(_userDataManager.GetUserData(userId, i))) + .ToList(); + + var info = new UserDataChangeInfo + { + UserId = userId.ToString("N"), + + UserDataList = dtoList + }; + + foreach (var userSession in userSessions) + { + try + { + await userSession.SessionController.SendUserDataChangeInfo(info, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error sending UserDataChanged message", ex); + } + } + } + + } + } + + public void Dispose() + { + if (UpdateTimer != null) + { + UpdateTimer.Dispose(); + UpdateTimer = null; + } + + _userDataManager.UserDataSaved -= _userDataManager_UserDataSaved; + } + } +} diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index b0f7553eab..86ea3b0510 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -115,7 +115,8 @@ - + + diff --git a/MediaBrowser.Server.Implementations/Session/SessionManager.cs b/MediaBrowser.Server.Implementations/Session/SessionManager.cs index ac69b0dc53..6bb6edf7a3 100644 --- a/MediaBrowser.Server.Implementations/Session/SessionManager.cs +++ b/MediaBrowser.Server.Implementations/Session/SessionManager.cs @@ -290,6 +290,7 @@ namespace MediaBrowser.Server.Implementations.Session var data = _userDataRepository.GetUserData(user.Id, key); UpdatePlayState(info.Item, data, info.PositionTicks.Value); + await _userDataRepository.SaveUserData(user.Id, key, data, UserDataSaveReason.PlaybackProgress, CancellationToken.None).ConfigureAwait(false); } @@ -501,7 +502,12 @@ namespace MediaBrowser.Server.Implementations.Session return session.SessionController.SendPlaystateCommand(command, cancellationToken); } - public Task SendRestartRequiredMessage(CancellationToken cancellationToken) + /// + /// Sends the restart required message. + /// + /// The cancellation token. + /// Task. + public Task SendRestartRequiredNotification(CancellationToken cancellationToken) { var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null).ToList(); @@ -509,11 +515,61 @@ namespace MediaBrowser.Server.Implementations.Session { try { - await session.SessionController.SendRestartRequiredMessage(cancellationToken).ConfigureAwait(false); + await session.SessionController.SendRestartRequiredNotification(cancellationToken).ConfigureAwait(false); } catch (Exception ex) { - _logger.ErrorException("Error in SendRestartRequiredMessage.", ex); + _logger.ErrorException("Error in SendRestartRequiredNotification.", ex); + } + + })); + + return Task.WhenAll(tasks); + } + + /// + /// Sends the server shutdown notification. + /// + /// The cancellation token. + /// Task. + public Task SendServerShutdownNotification(CancellationToken cancellationToken) + { + var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null).ToList(); + + var tasks = sessions.Select(session => Task.Run(async () => + { + try + { + await session.SessionController.SendServerShutdownNotification(cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error in SendServerShutdownNotification.", ex); + } + + })); + + return Task.WhenAll(tasks); + } + + /// + /// Sends the server restart notification. + /// + /// The cancellation token. + /// Task. + public Task SendServerRestartNotification(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.SendServerRestartNotification(cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error in SendServerRestartNotification.", ex); } })); diff --git a/MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs b/MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs index e90dd8eb95..399cce945f 100644 --- a/MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs @@ -84,15 +84,22 @@ namespace MediaBrowser.Server.Implementations.Session /// Processes the identity message. /// /// The message. - private void ProcessIdentityMessage(WebSocketMessageInfo message) + private async void ProcessIdentityMessage(WebSocketMessageInfo message) { - _logger.Debug("Received Identity message"); + _logger.Debug("Received Identity message: " + message.Data); var vals = message.Data.Split('|'); var client = vals[0]; var deviceId = vals[1]; var version = vals[2]; + var deviceName = vals.Length > 3 ? vals[3] : string.Empty; + + if (!string.IsNullOrEmpty(deviceName)) + { + _logger.Debug("Logging session activity"); + await _sessionManager.LogSessionActivity(client, version, deviceId, deviceName, null).ConfigureAwait(false); + } var session = _sessionManager.Sessions .FirstOrDefault(i => string.Equals(i.DeviceId, deviceId) && @@ -156,7 +163,7 @@ namespace MediaBrowser.Server.Implementations.Session if (result == null) { - _logger.Error("Unable to session based on web socket message"); + _logger.Error("Unable to find session based on web socket message"); } return result; diff --git a/MediaBrowser.Server.Implementations/Session/WebSocketController.cs b/MediaBrowser.Server.Implementations/Session/WebSocketController.cs index 46c8f752dd..6bbebf156c 100644 --- a/MediaBrowser.Server.Implementations/Session/WebSocketController.cs +++ b/MediaBrowser.Server.Implementations/Session/WebSocketController.cs @@ -134,7 +134,7 @@ namespace MediaBrowser.Server.Implementations.Session /// /// The cancellation token. /// Task. - public Task SendRestartRequiredMessage(CancellationToken cancellationToken) + public Task SendRestartRequiredNotification(CancellationToken cancellationToken) { var socket = GetActiveSocket(); @@ -145,5 +145,58 @@ namespace MediaBrowser.Server.Implementations.Session }, cancellationToken); } + + + /// + /// Sends the user data change info. + /// + /// The info. + /// The cancellation token. + /// Task. + public Task SendUserDataChangeInfo(UserDataChangeInfo info, CancellationToken cancellationToken) + { + var socket = GetActiveSocket(); + + return socket.SendAsync(new WebSocketMessage + { + MessageType = "UserDataChanged", + Data = info + + }, cancellationToken); + } + + /// + /// Sends the server shutdown notification. + /// + /// The cancellation token. + /// Task. + public Task SendServerShutdownNotification(CancellationToken cancellationToken) + { + var socket = GetActiveSocket(); + + return socket.SendAsync(new WebSocketMessage + { + MessageType = "ServerShuttingDown", + Data = string.Empty + + }, cancellationToken); + } + + /// + /// Sends the server restart notification. + /// + /// The cancellation token. + /// Task. + public Task SendServerRestartNotification(CancellationToken cancellationToken) + { + var socket = GetActiveSocket(); + + return socket.SendAsync(new WebSocketMessage + { + MessageType = "ServerRestarting", + Data = string.Empty + + }, cancellationToken); + } } } diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index a10b7c15b8..87bd45e507 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -488,11 +488,11 @@ namespace MediaBrowser.ServerApplication { try { - await ServerManager.SendWebSocketMessageAsync("ServerRestarting", () => string.Empty, CancellationToken.None).ConfigureAwait(false); + await SessionManager.SendServerRestartNotification(CancellationToken.None).ConfigureAwait(false); } catch (Exception ex) { - Logger.ErrorException("Error sending server restart web socket message", ex); + Logger.ErrorException("Error sending server restart notification", ex); } NativeApp.Restart(); @@ -609,11 +609,11 @@ namespace MediaBrowser.ServerApplication { try { - await ServerManager.SendWebSocketMessageAsync("ServerShuttingDown", () => string.Empty, CancellationToken.None).ConfigureAwait(false); + await SessionManager.SendServerShutdownNotification(CancellationToken.None).ConfigureAwait(false); } catch (Exception ex) { - Logger.ErrorException("Error sending server shutdown web socket message", ex); + Logger.ErrorException("Error sending server shutdown notification", ex); } NativeApp.Shutdown(); diff --git a/MediaBrowser.WebDashboard/ApiClient.js b/MediaBrowser.WebDashboard/ApiClient.js index 0ef566fbf6..38a8a3136b 100644 --- a/MediaBrowser.WebDashboard/ApiClient.js +++ b/MediaBrowser.WebDashboard/ApiClient.js @@ -170,7 +170,7 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi webSocket.onopen = function () { setTimeout(function () { - self.sendWebSocketMessage("Identity", clientName + "|" + deviceId + "|" + applicationVersion); + self.sendWebSocketMessage("Identity", clientName + "|" + deviceId + "|" + applicationVersion + "|" + deviceName); $(self).trigger("websocketopen"); }, 500); diff --git a/MediaBrowser.WebDashboard/packages.config b/MediaBrowser.WebDashboard/packages.config index 3e9d964521..aec335395f 100644 --- a/MediaBrowser.WebDashboard/packages.config +++ b/MediaBrowser.WebDashboard/packages.config @@ -1,6 +1,6 @@  - + \ No newline at end of file