aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/Notifications
diff options
context:
space:
mode:
authorLuke <luke.pulverenti@gmail.com>2016-12-18 00:44:33 -0500
committerGitHub <noreply@github.com>2016-12-18 00:44:33 -0500
commite7cebb91a73354dc3e0d0b6340c9fbd6511f4406 (patch)
tree6f1c368c766c17b7514fe749c0e92e69cd89194a /Emby.Server.Implementations/Notifications
parent025905a3e4d50b9a2e07fbf4ff0a203af6604ced (diff)
parentaaa027f3229073e9a40756c3157d41af2a442922 (diff)
Merge pull request #2350 from MediaBrowser/beta
Beta
Diffstat (limited to 'Emby.Server.Implementations/Notifications')
-rw-r--r--Emby.Server.Implementations/Notifications/CoreNotificationTypes.cs198
-rw-r--r--Emby.Server.Implementations/Notifications/IConfigurableNotificationService.cs8
-rw-r--r--Emby.Server.Implementations/Notifications/InternalNotificationService.cs61
-rw-r--r--Emby.Server.Implementations/Notifications/NotificationConfigurationFactory.cs21
-rw-r--r--Emby.Server.Implementations/Notifications/NotificationManager.cs296
-rw-r--r--Emby.Server.Implementations/Notifications/Notifications.cs547
-rw-r--r--Emby.Server.Implementations/Notifications/SqliteNotificationsRepository.cs337
-rw-r--r--Emby.Server.Implementations/Notifications/WebSocketNotifier.cs54
8 files changed, 1522 insertions, 0 deletions
diff --git a/Emby.Server.Implementations/Notifications/CoreNotificationTypes.cs b/Emby.Server.Implementations/Notifications/CoreNotificationTypes.cs
new file mode 100644
index 000000000..f9fb98f85
--- /dev/null
+++ b/Emby.Server.Implementations/Notifications/CoreNotificationTypes.cs
@@ -0,0 +1,198 @@
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Notifications;
+using MediaBrowser.Model.Notifications;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Model.Globalization;
+
+namespace Emby.Server.Implementations.Notifications
+{
+ public class CoreNotificationTypes : INotificationTypeFactory
+ {
+ private readonly ILocalizationManager _localization;
+ private readonly IServerApplicationHost _appHost;
+
+ public CoreNotificationTypes(ILocalizationManager localization, IServerApplicationHost appHost)
+ {
+ _localization = localization;
+ _appHost = appHost;
+ }
+
+ public IEnumerable<NotificationTypeInfo> GetNotificationTypes()
+ {
+ var knownTypes = new List<NotificationTypeInfo>
+ {
+ new NotificationTypeInfo
+ {
+ Type = NotificationType.ApplicationUpdateInstalled.ToString(),
+ DefaultDescription = "{ReleaseNotes}",
+ DefaultTitle = "A new version of Emby Server has been installed.",
+ Variables = new List<string>{"Version"}
+ },
+
+ new NotificationTypeInfo
+ {
+ Type = NotificationType.InstallationFailed.ToString(),
+ DefaultTitle = "{Name} installation failed.",
+ Variables = new List<string>{"Name", "Version"}
+ },
+
+ new NotificationTypeInfo
+ {
+ Type = NotificationType.PluginInstalled.ToString(),
+ DefaultTitle = "{Name} was installed.",
+ Variables = new List<string>{"Name", "Version"}
+ },
+
+ new NotificationTypeInfo
+ {
+ Type = NotificationType.PluginError.ToString(),
+ DefaultTitle = "{Name} has encountered an error.",
+ DefaultDescription = "{ErrorMessage}",
+ Variables = new List<string>{"Name", "ErrorMessage"}
+ },
+
+ new NotificationTypeInfo
+ {
+ Type = NotificationType.PluginUninstalled.ToString(),
+ DefaultTitle = "{Name} was uninstalled.",
+ Variables = new List<string>{"Name", "Version"}
+ },
+
+ new NotificationTypeInfo
+ {
+ Type = NotificationType.PluginUpdateInstalled.ToString(),
+ DefaultTitle = "{Name} was updated.",
+ DefaultDescription = "{ReleaseNotes}",
+ Variables = new List<string>{"Name", "ReleaseNotes", "Version"}
+ },
+
+ new NotificationTypeInfo
+ {
+ Type = NotificationType.ServerRestartRequired.ToString(),
+ DefaultTitle = "Please restart Emby Server to finish updating."
+ },
+
+ new NotificationTypeInfo
+ {
+ Type = NotificationType.TaskFailed.ToString(),
+ DefaultTitle = "{Name} failed.",
+ DefaultDescription = "{ErrorMessage}",
+ Variables = new List<string>{"Name", "ErrorMessage"}
+ },
+
+ new NotificationTypeInfo
+ {
+ Type = NotificationType.NewLibraryContent.ToString(),
+ DefaultTitle = "{Name} has been added to your media library.",
+ Variables = new List<string>{"Name"}
+ },
+
+ new NotificationTypeInfo
+ {
+ Type = NotificationType.AudioPlayback.ToString(),
+ DefaultTitle = "{UserName} is playing {ItemName} on {DeviceName}.",
+ Variables = new List<string>{"UserName", "ItemName", "DeviceName", "AppName"}
+ },
+
+ new NotificationTypeInfo
+ {
+ Type = NotificationType.GamePlayback.ToString(),
+ DefaultTitle = "{UserName} is playing {ItemName} on {DeviceName}.",
+ Variables = new List<string>{"UserName", "ItemName", "DeviceName", "AppName"}
+ },
+
+ new NotificationTypeInfo
+ {
+ Type = NotificationType.VideoPlayback.ToString(),
+ DefaultTitle = "{UserName} is playing {ItemName} on {DeviceName}.",
+ Variables = new List<string>{"UserName", "ItemName", "DeviceName", "AppName"}
+ },
+
+ new NotificationTypeInfo
+ {
+ Type = NotificationType.AudioPlaybackStopped.ToString(),
+ DefaultTitle = "{UserName} has finished playing {ItemName} on {DeviceName}.",
+ Variables = new List<string>{"UserName", "ItemName", "DeviceName", "AppName"}
+ },
+
+ new NotificationTypeInfo
+ {
+ Type = NotificationType.GamePlaybackStopped.ToString(),
+ DefaultTitle = "{UserName} has finished playing {ItemName} on {DeviceName}.",
+ Variables = new List<string>{"UserName", "ItemName", "DeviceName", "AppName"}
+ },
+
+ new NotificationTypeInfo
+ {
+ Type = NotificationType.VideoPlaybackStopped.ToString(),
+ DefaultTitle = "{UserName} has finished playing {ItemName} on {DeviceName}.",
+ Variables = new List<string>{"UserName", "ItemName", "DeviceName", "AppName"}
+ },
+
+ new NotificationTypeInfo
+ {
+ Type = NotificationType.CameraImageUploaded.ToString(),
+ DefaultTitle = "A new camera image has been uploaded from {DeviceName}.",
+ Variables = new List<string>{"DeviceName"}
+ },
+
+ new NotificationTypeInfo
+ {
+ Type = NotificationType.UserLockedOut.ToString(),
+ DefaultTitle = "{UserName} has been locked out.",
+ Variables = new List<string>{"UserName"}
+ }
+ };
+
+ if (!_appHost.CanSelfUpdate)
+ {
+ knownTypes.Add(new NotificationTypeInfo
+ {
+ Type = NotificationType.ApplicationUpdateAvailable.ToString(),
+ DefaultTitle = "A new version of Emby Server is available for download."
+ });
+ }
+
+ foreach (var type in knownTypes)
+ {
+ Update(type);
+ }
+
+ var systemName = _localization.GetLocalizedString("CategorySystem");
+
+ return knownTypes.OrderByDescending(i => string.Equals(i.Category, systemName, StringComparison.OrdinalIgnoreCase))
+ .ThenBy(i => i.Category)
+ .ThenBy(i => i.Name);
+ }
+
+ private void Update(NotificationTypeInfo note)
+ {
+ note.Name = _localization.GetLocalizedString("NotificationOption" + note.Type) ?? note.Type;
+
+ note.IsBasedOnUserEvent = note.Type.IndexOf("Playback", StringComparison.OrdinalIgnoreCase) != -1;
+
+ if (note.Type.IndexOf("Playback", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ note.Category = _localization.GetLocalizedString("CategoryUser");
+ }
+ else if (note.Type.IndexOf("Plugin", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ note.Category = _localization.GetLocalizedString("CategoryPlugin");
+ }
+ else if (note.Type.IndexOf("CameraImageUploaded", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ note.Category = _localization.GetLocalizedString("CategorySync");
+ }
+ else if (note.Type.IndexOf("UserLockedOut", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ note.Category = _localization.GetLocalizedString("CategoryUser");
+ }
+ else
+ {
+ note.Category = _localization.GetLocalizedString("CategorySystem");
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Notifications/IConfigurableNotificationService.cs b/Emby.Server.Implementations/Notifications/IConfigurableNotificationService.cs
new file mode 100644
index 000000000..d74667c48
--- /dev/null
+++ b/Emby.Server.Implementations/Notifications/IConfigurableNotificationService.cs
@@ -0,0 +1,8 @@
+namespace Emby.Server.Implementations.Notifications
+{
+ public interface IConfigurableNotificationService
+ {
+ bool IsHidden { get; }
+ bool IsEnabled(string notificationType);
+ }
+}
diff --git a/Emby.Server.Implementations/Notifications/InternalNotificationService.cs b/Emby.Server.Implementations/Notifications/InternalNotificationService.cs
new file mode 100644
index 000000000..61c564f18
--- /dev/null
+++ b/Emby.Server.Implementations/Notifications/InternalNotificationService.cs
@@ -0,0 +1,61 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Notifications;
+using MediaBrowser.Model.Notifications;
+using System.Threading;
+using System.Threading.Tasks;
+using System;
+
+namespace Emby.Server.Implementations.Notifications
+{
+ public class InternalNotificationService : INotificationService, IConfigurableNotificationService
+ {
+ private readonly INotificationsRepository _repo;
+
+ public InternalNotificationService(INotificationsRepository repo)
+ {
+ _repo = repo;
+ }
+
+ public string Name
+ {
+ get { return "Dashboard Notifications"; }
+ }
+
+ public Task SendNotification(UserNotification request, CancellationToken cancellationToken)
+ {
+ return _repo.AddNotification(new Notification
+ {
+ Date = request.Date,
+ Description = request.Description,
+ Level = request.Level,
+ Name = request.Name,
+ Url = request.Url,
+ UserId = request.User.Id.ToString("N")
+
+ }, cancellationToken);
+ }
+
+ public bool IsEnabledForUser(User user)
+ {
+ return user.Policy.IsAdministrator;
+ }
+
+ public bool IsHidden
+ {
+ get { return true; }
+ }
+
+ public bool IsEnabled(string notificationType)
+ {
+ if (notificationType.IndexOf("playback", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return false;
+ }
+ if (notificationType.IndexOf("newlibrarycontent", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return false;
+ }
+ return true;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Notifications/NotificationConfigurationFactory.cs b/Emby.Server.Implementations/Notifications/NotificationConfigurationFactory.cs
new file mode 100644
index 000000000..a7c5b1233
--- /dev/null
+++ b/Emby.Server.Implementations/Notifications/NotificationConfigurationFactory.cs
@@ -0,0 +1,21 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Notifications;
+using System.Collections.Generic;
+
+namespace Emby.Server.Implementations.Notifications
+{
+ public class NotificationConfigurationFactory : IConfigurationFactory
+ {
+ public IEnumerable<ConfigurationStore> GetConfigurations()
+ {
+ return new List<ConfigurationStore>
+ {
+ new ConfigurationStore
+ {
+ Key = "notifications",
+ ConfigurationType = typeof (NotificationOptions)
+ }
+ };
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Notifications/NotificationManager.cs b/Emby.Server.Implementations/Notifications/NotificationManager.cs
new file mode 100644
index 000000000..db7980497
--- /dev/null
+++ b/Emby.Server.Implementations/Notifications/NotificationManager.cs
@@ -0,0 +1,296 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Notifications;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Notifications;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Extensions;
+
+namespace Emby.Server.Implementations.Notifications
+{
+ public class NotificationManager : INotificationManager
+ {
+ private readonly ILogger _logger;
+ private readonly IUserManager _userManager;
+ private readonly IServerConfigurationManager _config;
+
+ private INotificationService[] _services;
+ private INotificationTypeFactory[] _typeFactories;
+
+ public NotificationManager(ILogManager logManager, IUserManager userManager, IServerConfigurationManager config)
+ {
+ _userManager = userManager;
+ _config = config;
+ _logger = logManager.GetLogger(GetType().Name);
+ }
+
+ private NotificationOptions GetConfiguration()
+ {
+ return _config.GetConfiguration<NotificationOptions>("notifications");
+ }
+
+ public Task SendNotification(NotificationRequest request, CancellationToken cancellationToken)
+ {
+ var notificationType = request.NotificationType;
+
+ var options = string.IsNullOrWhiteSpace(notificationType) ?
+ null :
+ GetConfiguration().GetOptions(notificationType);
+
+ var users = GetUserIds(request, options)
+ .Select(i => _userManager.GetUserById(i));
+
+ var title = GetTitle(request, options);
+ var description = GetDescription(request, options);
+
+ var tasks = _services.Where(i => IsEnabled(i, notificationType))
+ .Select(i => SendNotification(request, i, users, title, description, cancellationToken));
+
+ return Task.WhenAll(tasks);
+ }
+
+ private Task SendNotification(NotificationRequest request,
+ INotificationService service,
+ IEnumerable<User> users,
+ string title,
+ string description,
+ CancellationToken cancellationToken)
+ {
+ users = users.Where(i => IsEnabledForUser(service, i))
+ .ToList();
+
+ var tasks = users.Select(i => SendNotification(request, service, title, description, i, cancellationToken));
+
+ return Task.WhenAll(tasks);
+
+ }
+
+ private IEnumerable<string> GetUserIds(NotificationRequest request, NotificationOption options)
+ {
+ if (request.SendToUserMode.HasValue)
+ {
+ switch (request.SendToUserMode.Value)
+ {
+ case SendToUserType.Admins:
+ return _userManager.Users.Where(i => i.Policy.IsAdministrator)
+ .Select(i => i.Id.ToString("N"));
+ case SendToUserType.All:
+ return _userManager.Users.Select(i => i.Id.ToString("N"));
+ case SendToUserType.Custom:
+ return request.UserIds;
+ default:
+ throw new ArgumentException("Unrecognized SendToUserMode: " + request.SendToUserMode.Value);
+ }
+ }
+
+ if (options != null && !string.IsNullOrWhiteSpace(request.NotificationType))
+ {
+ var config = GetConfiguration();
+
+ return _userManager.Users
+ .Where(i => config.IsEnabledToSendToUser(request.NotificationType, i.Id.ToString("N"), i.Policy))
+ .Select(i => i.Id.ToString("N"));
+ }
+
+ return request.UserIds;
+ }
+
+ private async Task SendNotification(NotificationRequest request,
+ INotificationService service,
+ string title,
+ string description,
+ User user,
+ CancellationToken cancellationToken)
+ {
+ var notification = new UserNotification
+ {
+ Date = request.Date,
+ Description = description,
+ Level = request.Level,
+ Name = title,
+ Url = request.Url,
+ User = user
+ };
+
+ _logger.Debug("Sending notification via {0} to user {1}", service.Name, user.Name);
+
+ try
+ {
+ await service.SendNotification(notification, cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error sending notification to {0}", ex, service.Name);
+ }
+ }
+
+ private string GetTitle(NotificationRequest request, NotificationOption options)
+ {
+ var title = request.Name;
+
+ // If empty, grab from options
+ if (string.IsNullOrEmpty(title))
+ {
+ if (!string.IsNullOrEmpty(request.NotificationType))
+ {
+ if (options != null)
+ {
+ title = options.Title;
+ }
+ }
+ }
+
+ // If still empty, grab default
+ if (string.IsNullOrEmpty(title))
+ {
+ if (!string.IsNullOrEmpty(request.NotificationType))
+ {
+ var info = GetNotificationTypes().FirstOrDefault(i => string.Equals(i.Type, request.NotificationType, StringComparison.OrdinalIgnoreCase));
+
+ if (info != null)
+ {
+ title = info.DefaultTitle;
+ }
+ }
+ }
+
+ title = title ?? string.Empty;
+
+ foreach (var pair in request.Variables)
+ {
+ var token = "{" + pair.Key + "}";
+
+ title = title.Replace(token, pair.Value, StringComparison.OrdinalIgnoreCase);
+ }
+
+ return title;
+ }
+
+ private string GetDescription(NotificationRequest request, NotificationOption options)
+ {
+ var text = request.Description;
+
+ // If empty, grab from options
+ if (string.IsNullOrEmpty(text))
+ {
+ if (!string.IsNullOrEmpty(request.NotificationType))
+ {
+ if (options != null)
+ {
+ text = options.Description;
+ }
+ }
+ }
+
+ // If still empty, grab default
+ if (string.IsNullOrEmpty(text))
+ {
+ if (!string.IsNullOrEmpty(request.NotificationType))
+ {
+ var info = GetNotificationTypes().FirstOrDefault(i => string.Equals(i.Type, request.NotificationType, StringComparison.OrdinalIgnoreCase));
+
+ if (info != null)
+ {
+ text = info.DefaultDescription;
+ }
+ }
+ }
+
+ text = text ?? string.Empty;
+
+ foreach (var pair in request.Variables)
+ {
+ var token = "{" + pair.Key + "}";
+
+ text = text.Replace(token, pair.Value, StringComparison.OrdinalIgnoreCase);
+ }
+
+ return text;
+ }
+
+ private bool IsEnabledForUser(INotificationService service, User user)
+ {
+ try
+ {
+ return service.IsEnabledForUser(user);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error in IsEnabledForUser", ex);
+ return false;
+ }
+ }
+
+ private bool IsEnabled(INotificationService service, string notificationType)
+ {
+ if (string.IsNullOrEmpty(notificationType))
+ {
+ return true;
+ }
+
+ var configurable = service as IConfigurableNotificationService;
+
+ if (configurable != null)
+ {
+ return configurable.IsEnabled(notificationType);
+ }
+
+ return GetConfiguration().IsServiceEnabled(service.Name, notificationType);
+ }
+
+ public void AddParts(IEnumerable<INotificationService> services, IEnumerable<INotificationTypeFactory> notificationTypeFactories)
+ {
+ _services = services.ToArray();
+ _typeFactories = notificationTypeFactories.ToArray();
+ }
+
+ public IEnumerable<NotificationTypeInfo> GetNotificationTypes()
+ {
+ var list = _typeFactories.Select(i =>
+ {
+ try
+ {
+ return i.GetNotificationTypes().ToList();
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error in GetNotificationTypes", ex);
+ return new List<NotificationTypeInfo>();
+ }
+
+ }).SelectMany(i => i).ToList();
+
+ var config = GetConfiguration();
+
+ foreach (var i in list)
+ {
+ i.Enabled = config.IsEnabled(i.Type);
+ }
+
+ return list;
+ }
+
+ public IEnumerable<NotificationServiceInfo> GetNotificationServices()
+ {
+ return _services.Where(i =>
+ {
+ var configurable = i as IConfigurableNotificationService;
+
+ return configurable == null || !configurable.IsHidden;
+
+ }).Select(i => new NotificationServiceInfo
+ {
+ Name = i.Name,
+ Id = i.Name.GetMD5().ToString("N")
+
+ }).OrderBy(i => i.Name);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Notifications/Notifications.cs b/Emby.Server.Implementations/Notifications/Notifications.cs
new file mode 100644
index 000000000..2d441c18c
--- /dev/null
+++ b/Emby.Server.Implementations/Notifications/Notifications.cs
@@ -0,0 +1,547 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Common.Updates;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Notifications;
+using MediaBrowser.Controller.Plugins;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Notifications;
+using MediaBrowser.Model.Tasks;
+using MediaBrowser.Model.Updates;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Model.Threading;
+
+namespace Emby.Server.Implementations.Notifications
+{
+ /// <summary>
+ /// Creates notifications for various system events
+ /// </summary>
+ public class Notifications : IServerEntryPoint
+ {
+ private readonly IInstallationManager _installationManager;
+ private readonly IUserManager _userManager;
+ private readonly ILogger _logger;
+
+ private readonly ITaskManager _taskManager;
+ private readonly INotificationManager _notificationManager;
+
+ private readonly ILibraryManager _libraryManager;
+ private readonly ISessionManager _sessionManager;
+ private readonly IServerApplicationHost _appHost;
+ private readonly ITimerFactory _timerFactory;
+
+ private ITimer LibraryUpdateTimer { get; set; }
+ private readonly object _libraryChangedSyncLock = new object();
+
+ private readonly IConfigurationManager _config;
+ private readonly IDeviceManager _deviceManager;
+
+ public Notifications(IInstallationManager installationManager, IUserManager userManager, ILogger logger, ITaskManager taskManager, INotificationManager notificationManager, ILibraryManager libraryManager, ISessionManager sessionManager, IServerApplicationHost appHost, IConfigurationManager config, IDeviceManager deviceManager, ITimerFactory timerFactory)
+ {
+ _installationManager = installationManager;
+ _userManager = userManager;
+ _logger = logger;
+ _taskManager = taskManager;
+ _notificationManager = notificationManager;
+ _libraryManager = libraryManager;
+ _sessionManager = sessionManager;
+ _appHost = appHost;
+ _config = config;
+ _deviceManager = deviceManager;
+ _timerFactory = timerFactory;
+ }
+
+ public void Run()
+ {
+ _installationManager.PluginInstalled += _installationManager_PluginInstalled;
+ _installationManager.PluginUpdated += _installationManager_PluginUpdated;
+ _installationManager.PackageInstallationFailed += _installationManager_PackageInstallationFailed;
+ _installationManager.PluginUninstalled += _installationManager_PluginUninstalled;
+
+ _taskManager.TaskCompleted += _taskManager_TaskCompleted;
+
+ _userManager.UserCreated += _userManager_UserCreated;
+ _libraryManager.ItemAdded += _libraryManager_ItemAdded;
+ _sessionManager.PlaybackStart += _sessionManager_PlaybackStart;
+ _sessionManager.PlaybackStopped += _sessionManager_PlaybackStopped;
+ _appHost.HasPendingRestartChanged += _appHost_HasPendingRestartChanged;
+ _appHost.HasUpdateAvailableChanged += _appHost_HasUpdateAvailableChanged;
+ _appHost.ApplicationUpdated += _appHost_ApplicationUpdated;
+ _deviceManager.CameraImageUploaded += _deviceManager_CameraImageUploaded;
+
+ _userManager.UserLockedOut += _userManager_UserLockedOut;
+ }
+
+ async void _userManager_UserLockedOut(object sender, GenericEventArgs<User> e)
+ {
+ var type = NotificationType.UserLockedOut.ToString();
+
+ var notification = new NotificationRequest
+ {
+ NotificationType = type
+ };
+
+ notification.Variables["UserName"] = e.Argument.Name;
+
+ await SendNotification(notification).ConfigureAwait(false);
+ }
+
+ async void _deviceManager_CameraImageUploaded(object sender, GenericEventArgs<CameraImageUploadInfo> e)
+ {
+ var type = NotificationType.CameraImageUploaded.ToString();
+
+ var notification = new NotificationRequest
+ {
+ NotificationType = type
+ };
+
+ notification.Variables["DeviceName"] = e.Argument.Device.Name;
+
+ await SendNotification(notification).ConfigureAwait(false);
+ }
+
+ async void _appHost_ApplicationUpdated(object sender, GenericEventArgs<PackageVersionInfo> e)
+ {
+ var type = NotificationType.ApplicationUpdateInstalled.ToString();
+
+ var notification = new NotificationRequest
+ {
+ NotificationType = type,
+ Url = e.Argument.infoUrl
+ };
+
+ notification.Variables["Version"] = e.Argument.versionStr;
+ notification.Variables["ReleaseNotes"] = e.Argument.description;
+
+ await SendNotification(notification).ConfigureAwait(false);
+ }
+
+ async void _installationManager_PluginUpdated(object sender, GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>> e)
+ {
+ var type = NotificationType.PluginUpdateInstalled.ToString();
+
+ var installationInfo = e.Argument.Item1;
+
+ var notification = new NotificationRequest
+ {
+ Description = e.Argument.Item2.description,
+ NotificationType = type
+ };
+
+ notification.Variables["Name"] = installationInfo.Name;
+ notification.Variables["Version"] = installationInfo.Version.ToString();
+ notification.Variables["ReleaseNotes"] = e.Argument.Item2.description;
+
+ await SendNotification(notification).ConfigureAwait(false);
+ }
+
+ async void _installationManager_PluginInstalled(object sender, GenericEventArgs<PackageVersionInfo> e)
+ {
+ var type = NotificationType.PluginInstalled.ToString();
+
+ var installationInfo = e.Argument;
+
+ var notification = new NotificationRequest
+ {
+ Description = installationInfo.description,
+ NotificationType = type
+ };
+
+ notification.Variables["Name"] = installationInfo.name;
+ notification.Variables["Version"] = installationInfo.versionStr;
+
+ await SendNotification(notification).ConfigureAwait(false);
+ }
+
+ async void _appHost_HasUpdateAvailableChanged(object sender, EventArgs e)
+ {
+ // This notification is for users who can't auto-update (aka running as service)
+ if (!_appHost.HasUpdateAvailable || _appHost.CanSelfUpdate)
+ {
+ return;
+ }
+
+ var type = NotificationType.ApplicationUpdateAvailable.ToString();
+
+ var notification = new NotificationRequest
+ {
+ Description = "Please see emby.media for details.",
+ NotificationType = type
+ };
+
+ await SendNotification(notification).ConfigureAwait(false);
+ }
+
+ async void _appHost_HasPendingRestartChanged(object sender, EventArgs e)
+ {
+ if (!_appHost.HasPendingRestart)
+ {
+ return;
+ }
+
+ var type = NotificationType.ServerRestartRequired.ToString();
+
+ var notification = new NotificationRequest
+ {
+ NotificationType = type
+ };
+
+ await SendNotification(notification).ConfigureAwait(false);
+ }
+
+ private NotificationOptions GetOptions()
+ {
+ return _config.GetConfiguration<NotificationOptions>("notifications");
+ }
+
+ void _sessionManager_PlaybackStart(object sender, PlaybackProgressEventArgs e)
+ {
+ var item = e.MediaInfo;
+
+ if (item == null)
+ {
+ _logger.Warn("PlaybackStart reported with null media info.");
+ return;
+ }
+
+ var video = e.Item as Video;
+ if (video != null && video.IsThemeMedia)
+ {
+ return;
+ }
+
+ var type = GetPlaybackNotificationType(item.MediaType);
+
+ SendPlaybackNotification(type, e);
+ }
+
+ void _sessionManager_PlaybackStopped(object sender, PlaybackStopEventArgs e)
+ {
+ var item = e.MediaInfo;
+
+ if (item == null)
+ {
+ _logger.Warn("PlaybackStopped reported with null media info.");
+ return;
+ }
+
+ var video = e.Item as Video;
+ if (video != null && video.IsThemeMedia)
+ {
+ return;
+ }
+
+ var type = GetPlaybackStoppedNotificationType(item.MediaType);
+
+ SendPlaybackNotification(type, e);
+ }
+
+ private async void SendPlaybackNotification(string type, PlaybackProgressEventArgs e)
+ {
+ var user = e.Users.FirstOrDefault();
+
+ if (user != null && !GetOptions().IsEnabledToMonitorUser(type, user.Id.ToString("N")))
+ {
+ return;
+ }
+
+ var item = e.MediaInfo;
+
+ if ( item.IsThemeMedia)
+ {
+ // Don't report theme song or local trailer playback
+ return;
+ }
+
+ var notification = new NotificationRequest
+ {
+ NotificationType = type
+ };
+
+ if (e.Item != null)
+ {
+ notification.Variables["ItemName"] = GetItemName(e.Item);
+ }
+ else
+ {
+ notification.Variables["ItemName"] = item.Name;
+ }
+
+ notification.Variables["UserName"] = user == null ? "Unknown user" : user.Name;
+ notification.Variables["AppName"] = e.ClientName;
+ notification.Variables["DeviceName"] = e.DeviceName;
+
+ await SendNotification(notification).ConfigureAwait(false);
+ }
+
+ private string GetPlaybackNotificationType(string mediaType)
+ {
+ if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ {
+ return NotificationType.AudioPlayback.ToString();
+ }
+ if (string.Equals(mediaType, MediaType.Game, StringComparison.OrdinalIgnoreCase))
+ {
+ return NotificationType.GamePlayback.ToString();
+ }
+ if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ {
+ return NotificationType.VideoPlayback.ToString();
+ }
+
+ return null;
+ }
+
+ private string GetPlaybackStoppedNotificationType(string mediaType)
+ {
+ if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ {
+ return NotificationType.AudioPlaybackStopped.ToString();
+ }
+ if (string.Equals(mediaType, MediaType.Game, StringComparison.OrdinalIgnoreCase))
+ {
+ return NotificationType.GamePlaybackStopped.ToString();
+ }
+ if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ {
+ return NotificationType.VideoPlaybackStopped.ToString();
+ }
+
+ return null;
+ }
+
+ private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
+ void _libraryManager_ItemAdded(object sender, ItemChangeEventArgs e)
+ {
+ if (!FilterItem(e.Item))
+ {
+ return;
+ }
+
+ lock (_libraryChangedSyncLock)
+ {
+ if (LibraryUpdateTimer == null)
+ {
+ LibraryUpdateTimer = _timerFactory.Create(LibraryUpdateTimerCallback, null, 5000,
+ Timeout.Infinite);
+ }
+ else
+ {
+ LibraryUpdateTimer.Change(5000, Timeout.Infinite);
+ }
+
+ _itemsAdded.Add(e.Item);
+ }
+ }
+
+ private bool FilterItem(BaseItem item)
+ {
+ if (item.IsFolder)
+ {
+ return false;
+ }
+
+ if (item.LocationType == LocationType.Virtual)
+ {
+ return false;
+ }
+
+ if (item is IItemByName)
+ {
+ return false;
+ }
+
+ return item.SourceType == SourceType.Library;
+ }
+
+ private async void LibraryUpdateTimerCallback(object state)
+ {
+ List<BaseItem> items;
+
+ lock (_libraryChangedSyncLock)
+ {
+ items = _itemsAdded.ToList();
+ _itemsAdded.Clear();
+ DisposeLibraryUpdateTimer();
+ }
+
+ items = items.Take(10).ToList();
+
+ foreach (var item in items)
+ {
+ var notification = new NotificationRequest
+ {
+ NotificationType = NotificationType.NewLibraryContent.ToString()
+ };
+
+ notification.Variables["Name"] = GetItemName(item);
+
+ await SendNotification(notification).ConfigureAwait(false);
+ }
+ }
+
+ public static string GetItemName(BaseItem item)
+ {
+ var name = item.Name;
+ var episode = item as Episode;
+ if (episode != null)
+ {
+ if (episode.IndexNumber.HasValue)
+ {
+ name = string.Format("Ep{0} - {1}", episode.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), name);
+ }
+ if (episode.ParentIndexNumber.HasValue)
+ {
+ name = string.Format("S{0}, {1}", episode.ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture), name);
+ }
+ }
+
+ var hasSeries = item as IHasSeries;
+
+ if (hasSeries != null)
+ {
+ name = hasSeries.SeriesName + " - " + name;
+ }
+
+ var hasArtist = item as IHasArtist;
+ if (hasArtist != null)
+ {
+ var artists = hasArtist.AllArtists;
+
+ if (artists.Count > 0)
+ {
+ name = hasArtist.AllArtists[0] + " - " + name;
+ }
+ }
+
+ return name;
+ }
+
+ async void _userManager_UserCreated(object sender, GenericEventArgs<User> e)
+ {
+ var notification = new NotificationRequest
+ {
+ UserIds = new List<string> { e.Argument.Id.ToString("N") },
+ Name = "Welcome to Emby!",
+ Description = "Check back here for more notifications."
+ };
+
+ await SendNotification(notification).ConfigureAwait(false);
+ }
+
+ async void _taskManager_TaskCompleted(object sender, TaskCompletionEventArgs e)
+ {
+ var result = e.Result;
+
+ if (result.Status == TaskCompletionStatus.Failed)
+ {
+ var type = NotificationType.TaskFailed.ToString();
+
+ var notification = new NotificationRequest
+ {
+ Description = result.ErrorMessage,
+ Level = NotificationLevel.Error,
+ NotificationType = type
+ };
+
+ notification.Variables["Name"] = result.Name;
+ notification.Variables["ErrorMessage"] = result.ErrorMessage;
+
+ await SendNotification(notification).ConfigureAwait(false);
+ }
+ }
+
+ async void _installationManager_PluginUninstalled(object sender, GenericEventArgs<IPlugin> e)
+ {
+ var type = NotificationType.PluginUninstalled.ToString();
+
+ var plugin = e.Argument;
+
+ var notification = new NotificationRequest
+ {
+ NotificationType = type
+ };
+
+ notification.Variables["Name"] = plugin.Name;
+ notification.Variables["Version"] = plugin.Version.ToString();
+
+ await SendNotification(notification).ConfigureAwait(false);
+ }
+
+ async void _installationManager_PackageInstallationFailed(object sender, InstallationFailedEventArgs e)
+ {
+ var installationInfo = e.InstallationInfo;
+
+ var type = NotificationType.InstallationFailed.ToString();
+
+ var notification = new NotificationRequest
+ {
+ Level = NotificationLevel.Error,
+ Description = e.Exception.Message,
+ NotificationType = type
+ };
+
+ notification.Variables["Name"] = installationInfo.Name;
+ notification.Variables["Version"] = installationInfo.Version;
+
+ await SendNotification(notification).ConfigureAwait(false);
+ }
+
+ private async Task SendNotification(NotificationRequest notification)
+ {
+ try
+ {
+ await _notificationManager.SendNotification(notification, CancellationToken.None).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error sending notification", ex);
+ }
+ }
+
+ public void Dispose()
+ {
+ DisposeLibraryUpdateTimer();
+
+ _installationManager.PluginInstalled -= _installationManager_PluginInstalled;
+ _installationManager.PluginUpdated -= _installationManager_PluginUpdated;
+ _installationManager.PackageInstallationFailed -= _installationManager_PackageInstallationFailed;
+ _installationManager.PluginUninstalled -= _installationManager_PluginUninstalled;
+
+ _taskManager.TaskCompleted -= _taskManager_TaskCompleted;
+
+ _userManager.UserCreated -= _userManager_UserCreated;
+ _libraryManager.ItemAdded -= _libraryManager_ItemAdded;
+ _sessionManager.PlaybackStart -= _sessionManager_PlaybackStart;
+
+ _appHost.HasPendingRestartChanged -= _appHost_HasPendingRestartChanged;
+ _appHost.HasUpdateAvailableChanged -= _appHost_HasUpdateAvailableChanged;
+ _appHost.ApplicationUpdated -= _appHost_ApplicationUpdated;
+
+ _deviceManager.CameraImageUploaded -= _deviceManager_CameraImageUploaded;
+ _userManager.UserLockedOut -= _userManager_UserLockedOut;
+ }
+
+ private void DisposeLibraryUpdateTimer()
+ {
+ if (LibraryUpdateTimer != null)
+ {
+ LibraryUpdateTimer.Dispose();
+ LibraryUpdateTimer = null;
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Notifications/SqliteNotificationsRepository.cs b/Emby.Server.Implementations/Notifications/SqliteNotificationsRepository.cs
new file mode 100644
index 000000000..f18278cb2
--- /dev/null
+++ b/Emby.Server.Implementations/Notifications/SqliteNotificationsRepository.cs
@@ -0,0 +1,337 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Server.Implementations.Data;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Notifications;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Notifications;
+using SQLitePCL.pretty;
+
+namespace Emby.Server.Implementations.Notifications
+{
+ public class SqliteNotificationsRepository : BaseSqliteRepository, INotificationsRepository
+ {
+ public SqliteNotificationsRepository(ILogger logger, IServerApplicationPaths appPaths) : base(logger)
+ {
+ DbFilePath = Path.Combine(appPaths.DataPath, "notifications.db");
+ }
+
+ public event EventHandler<NotificationUpdateEventArgs> NotificationAdded;
+ public event EventHandler<NotificationReadEventArgs> NotificationsMarkedRead;
+ ////public event EventHandler<NotificationUpdateEventArgs> NotificationUpdated;
+
+ public void Initialize()
+ {
+ using (var connection = CreateConnection())
+ {
+ RunDefaultInitialization(connection);
+
+ string[] queries = {
+
+ "create table if not exists Notifications (Id GUID NOT NULL, UserId GUID NOT NULL, Date DATETIME NOT NULL, Name TEXT NOT NULL, Description TEXT NULL, Url TEXT NULL, Level TEXT NOT NULL, IsRead BOOLEAN NOT NULL, Category TEXT NOT NULL, RelatedId TEXT NULL, PRIMARY KEY (Id, UserId))",
+ "create index if not exists idx_Notifications1 on Notifications(Id)",
+ "create index if not exists idx_Notifications2 on Notifications(UserId)"
+ };
+
+ connection.RunQueries(queries);
+ }
+ }
+
+ /// <summary>
+ /// Gets the notifications.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <returns>NotificationResult.</returns>
+ public NotificationResult GetNotifications(NotificationQuery query)
+ {
+ var result = new NotificationResult();
+
+ var clauses = new List<string>();
+ var paramList = new List<object>();
+
+ if (query.IsRead.HasValue)
+ {
+ clauses.Add("IsRead=?");
+ paramList.Add(query.IsRead.Value);
+ }
+
+ clauses.Add("UserId=?");
+ paramList.Add(query.UserId.ToGuidParamValue());
+
+ var whereClause = " where " + string.Join(" And ", clauses.ToArray());
+
+ using (WriteLock.Read())
+ {
+ using (var connection = CreateConnection(true))
+ {
+ result.TotalRecordCount = connection.Query("select count(Id) from Notifications" + whereClause, paramList.ToArray()).SelectScalarInt().First();
+
+ var commandText = string.Format("select Id,UserId,Date,Name,Description,Url,Level,IsRead,Category,RelatedId from Notifications{0} order by IsRead asc, Date desc", whereClause);
+
+ if (query.Limit.HasValue || query.StartIndex.HasValue)
+ {
+ var offset = query.StartIndex ?? 0;
+
+ if (query.Limit.HasValue || offset > 0)
+ {
+ commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
+ }
+
+ if (offset > 0)
+ {
+ commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
+ }
+ }
+
+ var resultList = new List<Notification>();
+
+ foreach (var row in connection.Query(commandText, paramList.ToArray()))
+ {
+ resultList.Add(GetNotification(row));
+ }
+
+ result.Notifications = resultList.ToArray();
+ }
+ }
+
+ return result;
+ }
+
+ public NotificationsSummary GetNotificationsSummary(string userId)
+ {
+ var result = new NotificationsSummary();
+
+ using (WriteLock.Read())
+ {
+ using (var connection = CreateConnection(true))
+ {
+ using (var statement = connection.PrepareStatement("select Level from Notifications where UserId=@UserId and IsRead=@IsRead"))
+ {
+ statement.TryBind("@IsRead", false);
+ statement.TryBind("@UserId", userId.ToGuidParamValue());
+
+ var levels = new List<NotificationLevel>();
+
+ foreach (var row in statement.ExecuteQuery())
+ {
+ levels.Add(GetLevel(row, 0));
+ }
+
+ result.UnreadCount = levels.Count;
+
+ if (levels.Count > 0)
+ {
+ result.MaxUnreadNotificationLevel = levels.Max();
+ }
+ }
+
+ return result;
+ }
+ }
+ }
+
+ private Notification GetNotification(IReadOnlyList<IResultSetValue> reader)
+ {
+ var notification = new Notification
+ {
+ Id = reader[0].ReadGuid().ToString("N"),
+ UserId = reader[1].ReadGuid().ToString("N"),
+ Date = reader[2].ReadDateTime(),
+ Name = reader[3].ToString()
+ };
+
+ if (reader[4].SQLiteType != SQLiteType.Null)
+ {
+ notification.Description = reader[4].ToString();
+ }
+
+ if (reader[5].SQLiteType != SQLiteType.Null)
+ {
+ notification.Url = reader[5].ToString();
+ }
+
+ notification.Level = GetLevel(reader, 6);
+ notification.IsRead = reader[7].ToBool();
+
+ return notification;
+ }
+
+ /// <summary>
+ /// Gets the level.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <param name="index">The index.</param>
+ /// <returns>NotificationLevel.</returns>
+ private NotificationLevel GetLevel(IReadOnlyList<IResultSetValue> reader, int index)
+ {
+ NotificationLevel level;
+
+ var val = reader[index].ToString();
+
+ Enum.TryParse(val, true, out level);
+
+ return level;
+ }
+
+ /// <summary>
+ /// Adds the notification.
+ /// </summary>
+ /// <param name="notification">The notification.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task AddNotification(Notification notification, CancellationToken cancellationToken)
+ {
+ await ReplaceNotification(notification, cancellationToken).ConfigureAwait(false);
+
+ if (NotificationAdded != null)
+ {
+ try
+ {
+ NotificationAdded(this, new NotificationUpdateEventArgs
+ {
+ Notification = notification
+ });
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error in NotificationAdded event handler", ex);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Replaces the notification.
+ /// </summary>
+ /// <param name="notification">The notification.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ private async Task ReplaceNotification(Notification notification, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(notification.Id))
+ {
+ notification.Id = Guid.NewGuid().ToString("N");
+ }
+ if (string.IsNullOrEmpty(notification.UserId))
+ {
+ throw new ArgumentException("The notification must have a user id");
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ lock (WriteLock)
+ {
+ using (var connection = CreateConnection())
+ {
+ connection.RunInTransaction(conn =>
+ {
+ using (var statement = conn.PrepareStatement("replace into Notifications (Id, UserId, Date, Name, Description, Url, Level, IsRead, Category, RelatedId) values (@Id, @UserId, @Date, @Name, @Description, @Url, @Level, @IsRead, @Category, @RelatedId)"))
+ {
+ statement.TryBind("@Id", notification.Id.ToGuidParamValue());
+ statement.TryBind("@UserId", notification.UserId.ToGuidParamValue());
+ statement.TryBind("@Date", notification.Date.ToDateTimeParamValue());
+ statement.TryBind("@Name", notification.Name);
+ statement.TryBind("@Description", notification.Description);
+ statement.TryBind("@Url", notification.Url);
+ statement.TryBind("@Level", notification.Level.ToString());
+ statement.TryBind("@IsRead", notification.IsRead);
+ statement.TryBind("@Category", string.Empty);
+ statement.TryBind("@RelatedId", string.Empty);
+
+ statement.MoveNext();
+ }
+ }, TransactionMode);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Marks the read.
+ /// </summary>
+ /// <param name="notificationIdList">The notification id list.</param>
+ /// <param name="userId">The user id.</param>
+ /// <param name="isRead">if set to <c>true</c> [is read].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task MarkRead(IEnumerable<string> notificationIdList, string userId, bool isRead, CancellationToken cancellationToken)
+ {
+ var list = notificationIdList.ToList();
+ var idArray = list.Select(i => new Guid(i)).ToArray();
+
+ await MarkReadInternal(idArray, userId, isRead, cancellationToken).ConfigureAwait(false);
+
+ if (NotificationsMarkedRead != null)
+ {
+ try
+ {
+ NotificationsMarkedRead(this, new NotificationReadEventArgs
+ {
+ IdList = list.ToArray(),
+ IsRead = isRead,
+ UserId = userId
+ });
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error in NotificationsMarkedRead event handler", ex);
+ }
+ }
+ }
+
+ public async Task MarkAllRead(string userId, bool isRead, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ using (WriteLock.Write())
+ {
+ using (var connection = CreateConnection())
+ {
+ connection.RunInTransaction(conn =>
+ {
+ using (var statement = conn.PrepareStatement("update Notifications set IsRead=@IsRead where UserId=@UserId"))
+ {
+ statement.TryBind("@IsRead", isRead);
+ statement.TryBind("@UserId", userId.ToGuidParamValue());
+
+ statement.MoveNext();
+ }
+ }, TransactionMode);
+ }
+ }
+ }
+
+ private async Task MarkReadInternal(IEnumerable<Guid> notificationIdList, string userId, bool isRead, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ using (WriteLock.Write())
+ {
+ using (var connection = CreateConnection())
+ {
+ connection.RunInTransaction(conn =>
+ {
+ using (var statement = conn.PrepareStatement("update Notifications set IsRead=@IsRead where UserId=@UserId and Id=@Id"))
+ {
+ statement.TryBind("@IsRead", isRead);
+ statement.TryBind("@UserId", userId.ToGuidParamValue());
+
+ foreach (var id in notificationIdList)
+ {
+ statement.Reset();
+
+ statement.TryBind("@Id", id.ToGuidParamValue());
+
+ statement.MoveNext();
+ }
+ }
+
+ }, TransactionMode);
+ }
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Notifications/WebSocketNotifier.cs b/Emby.Server.Implementations/Notifications/WebSocketNotifier.cs
new file mode 100644
index 000000000..8b3367217
--- /dev/null
+++ b/Emby.Server.Implementations/Notifications/WebSocketNotifier.cs
@@ -0,0 +1,54 @@
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Notifications;
+using MediaBrowser.Controller.Plugins;
+using System.Linq;
+
+namespace Emby.Server.Implementations.Notifications
+{
+ /// <summary>
+ /// Notifies clients anytime a notification is added or udpated
+ /// </summary>
+ public class WebSocketNotifier : IServerEntryPoint
+ {
+ private readonly INotificationsRepository _notificationsRepo;
+
+ private readonly IServerManager _serverManager;
+
+ public WebSocketNotifier(INotificationsRepository notificationsRepo, IServerManager serverManager)
+ {
+ _notificationsRepo = notificationsRepo;
+ _serverManager = serverManager;
+ }
+
+ public void Run()
+ {
+ _notificationsRepo.NotificationAdded += _notificationsRepo_NotificationAdded;
+
+ _notificationsRepo.NotificationsMarkedRead += _notificationsRepo_NotificationsMarkedRead;
+ }
+
+ void _notificationsRepo_NotificationsMarkedRead(object sender, NotificationReadEventArgs e)
+ {
+ var list = e.IdList.ToList();
+
+ list.Add(e.UserId);
+ list.Add(e.IsRead.ToString().ToLower());
+
+ var msg = string.Join("|", list.ToArray());
+
+ _serverManager.SendWebSocketMessage("NotificationsMarkedRead", msg);
+ }
+
+ void _notificationsRepo_NotificationAdded(object sender, NotificationUpdateEventArgs e)
+ {
+ var msg = e.Notification.UserId + "|" + e.Notification.Id;
+
+ _serverManager.SendWebSocketMessage("NotificationAdded", msg);
+ }
+
+ public void Dispose()
+ {
+ _notificationsRepo.NotificationAdded -= _notificationsRepo_NotificationAdded;
+ }
+ }
+}