diff options
| author | Luke <luke.pulverenti@gmail.com> | 2016-11-03 19:59:50 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2016-11-03 19:59:50 -0400 |
| commit | c53745548ac2130f4cfbbe0d7a2804c36c8ae4eb (patch) | |
| tree | 6ee298ebb5470c4f3bcbef8d814a0354901469c4 /Emby.Server.Implementations/Notifications | |
| parent | 338b04a0c58729ec70aed89924ea6bd12422872b (diff) | |
| parent | 405a5f69c5967b4d919b5fe91396f12cb83e8aa8 (diff) | |
Merge pull request #2267 from MediaBrowser/dev
Dev
Diffstat (limited to 'Emby.Server.Implementations/Notifications')
7 files changed, 1185 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/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; + } + } +} |
