diff options
Diffstat (limited to 'Emby.Server.Implementations')
14 files changed, 3970 insertions, 0 deletions
diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs new file mode 100644 index 000000000..11fd3a872 --- /dev/null +++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs @@ -0,0 +1,561 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Common.Updates; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.Subtitles; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Tasks; +using MediaBrowser.Model.Updates; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using MediaBrowser.Model.Globalization; + +namespace Emby.Server.Implementations.Activity +{ + public class ActivityLogEntryPoint : IServerEntryPoint + { + private readonly IInstallationManager _installationManager; + + //private readonly ILogManager _logManager; + //private readonly ILogger _logger; + private readonly ISessionManager _sessionManager; + private readonly ITaskManager _taskManager; + private readonly IActivityManager _activityManager; + private readonly ILocalizationManager _localization; + + private readonly ILibraryManager _libraryManager; + private readonly ISubtitleManager _subManager; + private readonly IUserManager _userManager; + private readonly IServerConfigurationManager _config; + private readonly IServerApplicationHost _appHost; + + public ActivityLogEntryPoint(ISessionManager sessionManager, ITaskManager taskManager, IActivityManager activityManager, ILocalizationManager localization, IInstallationManager installationManager, ILibraryManager libraryManager, ISubtitleManager subManager, IUserManager userManager, IServerConfigurationManager config, IServerApplicationHost appHost) + { + //_logger = _logManager.GetLogger("ActivityLogEntryPoint"); + _sessionManager = sessionManager; + _taskManager = taskManager; + _activityManager = activityManager; + _localization = localization; + _installationManager = installationManager; + _libraryManager = libraryManager; + _subManager = subManager; + _userManager = userManager; + _config = config; + //_logManager = logManager; + _appHost = appHost; + } + + public void Run() + { + //_taskManager.TaskExecuting += _taskManager_TaskExecuting; + //_taskManager.TaskCompleted += _taskManager_TaskCompleted; + + //_installationManager.PluginInstalled += _installationManager_PluginInstalled; + //_installationManager.PluginUninstalled += _installationManager_PluginUninstalled; + //_installationManager.PluginUpdated += _installationManager_PluginUpdated; + + //_libraryManager.ItemAdded += _libraryManager_ItemAdded; + //_libraryManager.ItemRemoved += _libraryManager_ItemRemoved; + + _sessionManager.SessionStarted += _sessionManager_SessionStarted; + _sessionManager.AuthenticationFailed += _sessionManager_AuthenticationFailed; + _sessionManager.AuthenticationSucceeded += _sessionManager_AuthenticationSucceeded; + _sessionManager.SessionEnded += _sessionManager_SessionEnded; + + _sessionManager.PlaybackStart += _sessionManager_PlaybackStart; + _sessionManager.PlaybackStopped += _sessionManager_PlaybackStopped; + + //_subManager.SubtitlesDownloaded += _subManager_SubtitlesDownloaded; + _subManager.SubtitleDownloadFailure += _subManager_SubtitleDownloadFailure; + + _userManager.UserCreated += _userManager_UserCreated; + _userManager.UserPasswordChanged += _userManager_UserPasswordChanged; + _userManager.UserDeleted += _userManager_UserDeleted; + _userManager.UserConfigurationUpdated += _userManager_UserConfigurationUpdated; + _userManager.UserLockedOut += _userManager_UserLockedOut; + + //_config.ConfigurationUpdated += _config_ConfigurationUpdated; + //_config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated; + + //_logManager.LoggerLoaded += _logManager_LoggerLoaded; + + _appHost.ApplicationUpdated += _appHost_ApplicationUpdated; + } + + void _userManager_UserLockedOut(object sender, GenericEventArgs<User> e) + { + CreateLogEntry(new ActivityLogEntry + { + Name = string.Format(_localization.GetLocalizedString("UserLockedOutWithName"), e.Argument.Name), + Type = "UserLockedOut", + UserId = e.Argument.Id.ToString("N") + }); + } + + void _subManager_SubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e) + { + CreateLogEntry(new ActivityLogEntry + { + Name = string.Format(_localization.GetLocalizedString("SubtitleDownloadFailureForItem"), Notifications.Notifications.GetItemName(e.Item)), + Type = "SubtitleDownloadFailure", + ItemId = e.Item.Id.ToString("N"), + ShortOverview = string.Format(_localization.GetLocalizedString("ProviderValue"), e.Provider), + Overview = LogHelper.GetLogMessage(e.Exception).ToString() + }); + } + + void _sessionManager_PlaybackStopped(object sender, PlaybackStopEventArgs e) + { + var item = e.MediaInfo; + + if (item == null) + { + //_logger.Warn("PlaybackStopped reported with null media info."); + return; + } + + if (item.IsThemeMedia) + { + // Don't report theme song or local trailer playback + return; + } + + if (e.Users.Count == 0) + { + return; + } + + var user = e.Users.First(); + + CreateLogEntry(new ActivityLogEntry + { + Name = string.Format(_localization.GetLocalizedString("UserStoppedPlayingItemWithValues"), user.Name, item.Name), + Type = "PlaybackStopped", + ShortOverview = string.Format(_localization.GetLocalizedString("AppDeviceValues"), e.ClientName, e.DeviceName), + UserId = user.Id.ToString("N") + }); + } + + void _sessionManager_PlaybackStart(object sender, PlaybackProgressEventArgs e) + { + var item = e.MediaInfo; + + if (item == null) + { + //_logger.Warn("PlaybackStart reported with null media info."); + return; + } + + if (item.IsThemeMedia) + { + // Don't report theme song or local trailer playback + return; + } + + if (e.Users.Count == 0) + { + return; + } + + var user = e.Users.First(); + + CreateLogEntry(new ActivityLogEntry + { + Name = string.Format(_localization.GetLocalizedString("UserStartedPlayingItemWithValues"), user.Name, item.Name), + Type = "PlaybackStart", + ShortOverview = string.Format(_localization.GetLocalizedString("AppDeviceValues"), e.ClientName, e.DeviceName), + UserId = user.Id.ToString("N") + }); + } + + void _sessionManager_SessionEnded(object sender, SessionEventArgs e) + { + string name; + var session = e.SessionInfo; + + if (string.IsNullOrWhiteSpace(session.UserName)) + { + name = string.Format(_localization.GetLocalizedString("DeviceOfflineWithName"), session.DeviceName); + + // Causing too much spam for now + return; + } + else + { + name = string.Format(_localization.GetLocalizedString("UserOfflineFromDevice"), session.UserName, session.DeviceName); + } + + CreateLogEntry(new ActivityLogEntry + { + Name = name, + Type = "SessionEnded", + ShortOverview = string.Format(_localization.GetLocalizedString("LabelIpAddressValue"), session.RemoteEndPoint), + UserId = session.UserId.HasValue ? session.UserId.Value.ToString("N") : null + }); + } + + void _sessionManager_AuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationRequest> e) + { + CreateLogEntry(new ActivityLogEntry + { + Name = string.Format(_localization.GetLocalizedString("AuthenticationSucceededWithUserName"), e.Argument.Username), + Type = "AuthenticationSucceeded", + ShortOverview = string.Format(_localization.GetLocalizedString("LabelIpAddressValue"), e.Argument.RemoteEndPoint) + }); + } + + void _sessionManager_AuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e) + { + CreateLogEntry(new ActivityLogEntry + { + Name = string.Format(_localization.GetLocalizedString("FailedLoginAttemptWithUserName"), e.Argument.Username), + Type = "AuthenticationFailed", + ShortOverview = string.Format(_localization.GetLocalizedString("LabelIpAddressValue"), e.Argument.RemoteEndPoint), + Severity = LogSeverity.Error + }); + } + + void _appHost_ApplicationUpdated(object sender, GenericEventArgs<PackageVersionInfo> e) + { + CreateLogEntry(new ActivityLogEntry + { + Name = _localization.GetLocalizedString("MessageApplicationUpdated"), + Type = "ApplicationUpdated", + ShortOverview = string.Format(_localization.GetLocalizedString("VersionNumber"), e.Argument.versionStr), + Overview = e.Argument.description + }); + } + + void _logManager_LoggerLoaded(object sender, EventArgs e) + { + } + + void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) + { + CreateLogEntry(new ActivityLogEntry + { + Name = string.Format(_localization.GetLocalizedString("MessageNamedServerConfigurationUpdatedWithValue"), e.Key), + Type = "NamedConfigurationUpdated" + }); + } + + void _config_ConfigurationUpdated(object sender, EventArgs e) + { + CreateLogEntry(new ActivityLogEntry + { + Name = _localization.GetLocalizedString("MessageServerConfigurationUpdated"), + Type = "ServerConfigurationUpdated" + }); + } + + void _userManager_UserConfigurationUpdated(object sender, GenericEventArgs<User> e) + { + CreateLogEntry(new ActivityLogEntry + { + Name = string.Format(_localization.GetLocalizedString("UserConfigurationUpdatedWithName"), e.Argument.Name), + Type = "UserConfigurationUpdated", + UserId = e.Argument.Id.ToString("N") + }); + } + + void _userManager_UserDeleted(object sender, GenericEventArgs<User> e) + { + CreateLogEntry(new ActivityLogEntry + { + Name = string.Format(_localization.GetLocalizedString("UserDeletedWithName"), e.Argument.Name), + Type = "UserDeleted" + }); + } + + void _userManager_UserPasswordChanged(object sender, GenericEventArgs<User> e) + { + CreateLogEntry(new ActivityLogEntry + { + Name = string.Format(_localization.GetLocalizedString("UserPasswordChangedWithName"), e.Argument.Name), + Type = "UserPasswordChanged", + UserId = e.Argument.Id.ToString("N") + }); + } + + void _userManager_UserCreated(object sender, GenericEventArgs<User> e) + { + CreateLogEntry(new ActivityLogEntry + { + Name = string.Format(_localization.GetLocalizedString("UserCreatedWithName"), e.Argument.Name), + Type = "UserCreated", + UserId = e.Argument.Id.ToString("N") + }); + } + + void _subManager_SubtitlesDownloaded(object sender, SubtitleDownloadEventArgs e) + { + CreateLogEntry(new ActivityLogEntry + { + Name = string.Format(_localization.GetLocalizedString("SubtitlesDownloadedForItem"), Notifications.Notifications.GetItemName(e.Item)), + Type = "SubtitlesDownloaded", + ItemId = e.Item.Id.ToString("N"), + ShortOverview = string.Format(_localization.GetLocalizedString("ProviderValue"), e.Provider) + }); + } + + void _sessionManager_SessionStarted(object sender, SessionEventArgs e) + { + string name; + var session = e.SessionInfo; + + if (string.IsNullOrWhiteSpace(session.UserName)) + { + name = string.Format(_localization.GetLocalizedString("DeviceOnlineWithName"), session.DeviceName); + + // Causing too much spam for now + return; + } + else + { + name = string.Format(_localization.GetLocalizedString("UserOnlineFromDevice"), session.UserName, session.DeviceName); + } + + CreateLogEntry(new ActivityLogEntry + { + Name = name, + Type = "SessionStarted", + ShortOverview = string.Format(_localization.GetLocalizedString("LabelIpAddressValue"), session.RemoteEndPoint), + UserId = session.UserId.HasValue ? session.UserId.Value.ToString("N") : null + }); + } + + void _libraryManager_ItemRemoved(object sender, ItemChangeEventArgs e) + { + if (e.Item.SourceType != SourceType.Library) + { + return; + } + + CreateLogEntry(new ActivityLogEntry + { + Name = string.Format(_localization.GetLocalizedString("ItemRemovedWithName"), Notifications.Notifications.GetItemName(e.Item)), + Type = "ItemRemoved" + }); + } + + void _libraryManager_ItemAdded(object sender, ItemChangeEventArgs e) + { + if (e.Item.SourceType != SourceType.Library) + { + return; + } + + CreateLogEntry(new ActivityLogEntry + { + Name = string.Format(_localization.GetLocalizedString("ItemAddedWithName"), Notifications.Notifications.GetItemName(e.Item)), + Type = "ItemAdded", + ItemId = e.Item.Id.ToString("N") + }); + } + + void _installationManager_PluginUpdated(object sender, GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>> e) + { + CreateLogEntry(new ActivityLogEntry + { + Name = string.Format(_localization.GetLocalizedString("PluginUpdatedWithName"), e.Argument.Item1.Name), + Type = "PluginUpdated", + ShortOverview = string.Format(_localization.GetLocalizedString("VersionNumber"), e.Argument.Item2.versionStr), + Overview = e.Argument.Item2.description + }); + } + + void _installationManager_PluginUninstalled(object sender, GenericEventArgs<IPlugin> e) + { + CreateLogEntry(new ActivityLogEntry + { + Name = string.Format(_localization.GetLocalizedString("PluginUninstalledWithName"), e.Argument.Name), + Type = "PluginUninstalled" + }); + } + + void _installationManager_PluginInstalled(object sender, GenericEventArgs<PackageVersionInfo> e) + { + CreateLogEntry(new ActivityLogEntry + { + Name = string.Format(_localization.GetLocalizedString("PluginInstalledWithName"), e.Argument.name), + Type = "PluginInstalled", + ShortOverview = string.Format(_localization.GetLocalizedString("VersionNumber"), e.Argument.versionStr) + }); + } + + void _taskManager_TaskExecuting(object sender, GenericEventArgs<IScheduledTaskWorker> e) + { + var task = e.Argument; + + var activityTask = task.ScheduledTask as IConfigurableScheduledTask; + if (activityTask != null && !activityTask.IsLogged) + { + return; + } + + CreateLogEntry(new ActivityLogEntry + { + Name = string.Format(_localization.GetLocalizedString("ScheduledTaskStartedWithName"), task.Name), + Type = "ScheduledTaskStarted" + }); + } + + void _taskManager_TaskCompleted(object sender, TaskCompletionEventArgs e) + { + var result = e.Result; + var task = e.Task; + + var activityTask = task.ScheduledTask as IConfigurableScheduledTask; + if (activityTask != null && !activityTask.IsLogged) + { + return; + } + + var time = result.EndTimeUtc - result.StartTimeUtc; + var runningTime = string.Format(_localization.GetLocalizedString("LabelRunningTimeValue"), ToUserFriendlyString(time)); + + if (result.Status == TaskCompletionStatus.Failed) + { + var vals = new List<string>(); + + if (!string.IsNullOrWhiteSpace(e.Result.ErrorMessage)) + { + vals.Add(e.Result.ErrorMessage); + } + if (!string.IsNullOrWhiteSpace(e.Result.LongErrorMessage)) + { + vals.Add(e.Result.LongErrorMessage); + } + + CreateLogEntry(new ActivityLogEntry + { + Name = string.Format(_localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name), + Type = "ScheduledTaskFailed", + Overview = string.Join(Environment.NewLine, vals.ToArray()), + ShortOverview = runningTime, + Severity = LogSeverity.Error + }); + } + } + + private async void CreateLogEntry(ActivityLogEntry entry) + { + try + { + await _activityManager.Create(entry).ConfigureAwait(false); + } + catch + { + // Logged at lower levels + } + } + + public void Dispose() + { + _taskManager.TaskExecuting -= _taskManager_TaskExecuting; + _taskManager.TaskCompleted -= _taskManager_TaskCompleted; + + _installationManager.PluginInstalled -= _installationManager_PluginInstalled; + _installationManager.PluginUninstalled -= _installationManager_PluginUninstalled; + _installationManager.PluginUpdated -= _installationManager_PluginUpdated; + + _libraryManager.ItemAdded -= _libraryManager_ItemAdded; + _libraryManager.ItemRemoved -= _libraryManager_ItemRemoved; + + _sessionManager.SessionStarted -= _sessionManager_SessionStarted; + _sessionManager.AuthenticationFailed -= _sessionManager_AuthenticationFailed; + _sessionManager.AuthenticationSucceeded -= _sessionManager_AuthenticationSucceeded; + _sessionManager.SessionEnded -= _sessionManager_SessionEnded; + + _sessionManager.PlaybackStart -= _sessionManager_PlaybackStart; + _sessionManager.PlaybackStopped -= _sessionManager_PlaybackStopped; + + _subManager.SubtitlesDownloaded -= _subManager_SubtitlesDownloaded; + _subManager.SubtitleDownloadFailure -= _subManager_SubtitleDownloadFailure; + + _userManager.UserCreated -= _userManager_UserCreated; + _userManager.UserPasswordChanged -= _userManager_UserPasswordChanged; + _userManager.UserDeleted -= _userManager_UserDeleted; + _userManager.UserConfigurationUpdated -= _userManager_UserConfigurationUpdated; + _userManager.UserLockedOut -= _userManager_UserLockedOut; + + _config.ConfigurationUpdated -= _config_ConfigurationUpdated; + _config.NamedConfigurationUpdated -= _config_NamedConfigurationUpdated; + + //_logManager.LoggerLoaded -= _logManager_LoggerLoaded; + + _appHost.ApplicationUpdated -= _appHost_ApplicationUpdated; + } + + /// <summary> + /// Constructs a user-friendly string for this TimeSpan instance. + /// </summary> + public static string ToUserFriendlyString(TimeSpan span) + { + const int DaysInYear = 365; + const int DaysInMonth = 30; + + // Get each non-zero value from TimeSpan component + List<string> values = new List<string>(); + + // Number of years + int days = span.Days; + if (days >= DaysInYear) + { + int years = days / DaysInYear; + values.Add(CreateValueString(years, "year")); + days = days % DaysInYear; + } + // Number of months + if (days >= DaysInMonth) + { + int months = days / DaysInMonth; + values.Add(CreateValueString(months, "month")); + days = days % DaysInMonth; + } + // Number of days + if (days >= 1) + values.Add(CreateValueString(days, "day")); + // Number of hours + if (span.Hours >= 1) + values.Add(CreateValueString(span.Hours, "hour")); + // Number of minutes + if (span.Minutes >= 1) + values.Add(CreateValueString(span.Minutes, "minute")); + // Number of seconds (include when 0 if no other components included) + if (span.Seconds >= 1 || values.Count == 0) + values.Add(CreateValueString(span.Seconds, "second")); + + // Combine values into string + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < values.Count; i++) + { + if (builder.Length > 0) + builder.Append(i == values.Count - 1 ? " and " : ", "); + builder.Append(values[i]); + } + // Return result + return builder.ToString(); + } + + /// <summary> + /// Constructs a string description of a time-span value. + /// </summary> + /// <param name="value">The value of this item</param> + /// <param name="description">The name of this item (singular form)</param> + private static string CreateValueString(int value, string description) + { + return String.Format("{0:#,##0} {1}", + value, value == 1 ? description : String.Format("{0}s", description)); + } + } +} diff --git a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs new file mode 100644 index 000000000..b82d4e44e --- /dev/null +++ b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs @@ -0,0 +1,84 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Emby.Server.Implementations.Images; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Extensions; + +namespace Emby.Server.Implementations.Collections +{ + public class CollectionImageProvider : BaseDynamicImageProvider<BoxSet> + { + public CollectionImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor) : base(fileSystem, providerManager, applicationPaths, imageProcessor) + { + } + + protected override bool Supports(IHasImages item) + { + // Right now this is the only way to prevent this image from getting created ahead of internet image providers + if (!item.IsLocked) + { + return false; + } + + return base.Supports(item); + } + + protected override Task<List<BaseItem>> GetItemsWithImages(IHasImages item) + { + var playlist = (BoxSet)item; + + var items = playlist.Children.Concat(playlist.GetLinkedChildren()) + .Select(i => + { + var subItem = i; + + var episode = subItem as Episode; + + if (episode != null) + { + var series = episode.Series; + if (series != null && series.HasImage(ImageType.Primary)) + { + return series; + } + } + + if (subItem.HasImage(ImageType.Primary)) + { + return subItem; + } + + var parent = subItem.GetParent(); + + if (parent != null && parent.HasImage(ImageType.Primary)) + { + if (parent is MusicAlbum) + { + return parent; + } + } + + return null; + }) + .Where(i => i != null) + .DistinctBy(i => i.Id) + .ToList(); + + return Task.FromResult(GetFinalItems(items, 2)); + } + + protected override Task<string> CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + { + return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary); + } + } +} diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs new file mode 100644 index 000000000..cdf636e22 --- /dev/null +++ b/Emby.Server.Implementations/Devices/DeviceManager.cs @@ -0,0 +1,306 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Events; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Devices; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.Users; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Common.IO; +using MediaBrowser.Model.IO; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.IO; + +namespace Emby.Server.Implementations.Devices +{ + public class DeviceManager : IDeviceManager + { + private readonly IDeviceRepository _repo; + private readonly IUserManager _userManager; + private readonly IFileSystem _fileSystem; + private readonly ILibraryMonitor _libraryMonitor; + private readonly IServerConfigurationManager _config; + private readonly ILogger _logger; + private readonly INetworkManager _network; + + public event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded; + + /// <summary> + /// Occurs when [device options updated]. + /// </summary> + public event EventHandler<GenericEventArgs<DeviceInfo>> DeviceOptionsUpdated; + + public DeviceManager(IDeviceRepository repo, IUserManager userManager, IFileSystem fileSystem, ILibraryMonitor libraryMonitor, IServerConfigurationManager config, ILogger logger, INetworkManager network) + { + _repo = repo; + _userManager = userManager; + _fileSystem = fileSystem; + _libraryMonitor = libraryMonitor; + _config = config; + _logger = logger; + _network = network; + } + + public async Task<DeviceInfo> RegisterDevice(string reportedId, string name, string appName, string appVersion, string usedByUserId) + { + if (string.IsNullOrWhiteSpace(reportedId)) + { + throw new ArgumentNullException("reportedId"); + } + + var device = GetDevice(reportedId) ?? new DeviceInfo + { + Id = reportedId + }; + + device.ReportedName = name; + device.AppName = appName; + device.AppVersion = appVersion; + + if (!string.IsNullOrWhiteSpace(usedByUserId)) + { + var user = _userManager.GetUserById(usedByUserId); + + device.LastUserId = user.Id.ToString("N"); + device.LastUserName = user.Name; + } + + device.DateLastModified = DateTime.UtcNow; + + await _repo.SaveDevice(device).ConfigureAwait(false); + + return device; + } + + public Task SaveCapabilities(string reportedId, ClientCapabilities capabilities) + { + return _repo.SaveCapabilities(reportedId, capabilities); + } + + public ClientCapabilities GetCapabilities(string reportedId) + { + return _repo.GetCapabilities(reportedId); + } + + public DeviceInfo GetDevice(string id) + { + return _repo.GetDevice(id); + } + + public QueryResult<DeviceInfo> GetDevices(DeviceQuery query) + { + IEnumerable<DeviceInfo> devices = _repo.GetDevices().OrderByDescending(i => i.DateLastModified); + + if (query.SupportsContentUploading.HasValue) + { + var val = query.SupportsContentUploading.Value; + + devices = devices.Where(i => GetCapabilities(i.Id).SupportsContentUploading == val); + } + + if (query.SupportsSync.HasValue) + { + var val = query.SupportsSync.Value; + + devices = devices.Where(i => GetCapabilities(i.Id).SupportsSync == val); + } + + if (query.SupportsPersistentIdentifier.HasValue) + { + var val = query.SupportsPersistentIdentifier.Value; + + devices = devices.Where(i => + { + var caps = GetCapabilities(i.Id); + var deviceVal = caps.SupportsPersistentIdentifier; + return deviceVal == val; + }); + } + + if (!string.IsNullOrWhiteSpace(query.UserId)) + { + devices = devices.Where(i => CanAccessDevice(query.UserId, i.Id)); + } + + var array = devices.ToArray(); + return new QueryResult<DeviceInfo> + { + Items = array, + TotalRecordCount = array.Length + }; + } + + public Task DeleteDevice(string id) + { + return _repo.DeleteDevice(id); + } + + public ContentUploadHistory GetCameraUploadHistory(string deviceId) + { + return _repo.GetCameraUploadHistory(deviceId); + } + + public async Task AcceptCameraUpload(string deviceId, Stream stream, LocalFileInfo file) + { + var device = GetDevice(deviceId); + var path = GetUploadPath(device); + + if (!string.IsNullOrWhiteSpace(file.Album)) + { + path = Path.Combine(path, _fileSystem.GetValidFilename(file.Album)); + } + + path = Path.Combine(path, file.Name); + path = Path.ChangeExtension(path, MimeTypes.ToExtension(file.MimeType) ?? "jpg"); + + _libraryMonitor.ReportFileSystemChangeBeginning(path); + + _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); + + try + { + using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) + { + await stream.CopyToAsync(fs).ConfigureAwait(false); + } + + _repo.AddCameraUpload(deviceId, file); + } + finally + { + _libraryMonitor.ReportFileSystemChangeComplete(path, true); + } + + if (CameraImageUploaded != null) + { + EventHelper.FireEventIfNotNull(CameraImageUploaded, this, new GenericEventArgs<CameraImageUploadInfo> + { + Argument = new CameraImageUploadInfo + { + Device = device, + FileInfo = file + } + }, _logger); + } + } + + private string GetUploadPath(DeviceInfo device) + { + if (!string.IsNullOrWhiteSpace(device.CameraUploadPath)) + { + return device.CameraUploadPath; + } + + var config = _config.GetUploadOptions(); + if (!string.IsNullOrWhiteSpace(config.CameraUploadPath)) + { + return config.CameraUploadPath; + } + + var path = DefaultCameraUploadsPath; + + if (config.EnableCameraUploadSubfolders) + { + path = Path.Combine(path, _fileSystem.GetValidFilename(device.Name)); + } + + return path; + } + + private string DefaultCameraUploadsPath + { + get { return Path.Combine(_config.CommonApplicationPaths.DataPath, "camerauploads"); } + } + + public async Task UpdateDeviceInfo(string id, DeviceOptions options) + { + var device = GetDevice(id); + + device.CustomName = options.CustomName; + device.CameraUploadPath = options.CameraUploadPath; + + await _repo.SaveDevice(device).ConfigureAwait(false); + + EventHelper.FireEventIfNotNull(DeviceOptionsUpdated, this, new GenericEventArgs<DeviceInfo>(device), _logger); + } + + public bool CanAccessDevice(string userId, string deviceId) + { + if (string.IsNullOrWhiteSpace(userId)) + { + throw new ArgumentNullException("userId"); + } + if (string.IsNullOrWhiteSpace(deviceId)) + { + throw new ArgumentNullException("deviceId"); + } + + var user = _userManager.GetUserById(userId); + + if (user == null) + { + throw new ArgumentException("user not found"); + } + + if (!CanAccessDevice(user.Policy, deviceId)) + { + var capabilities = GetCapabilities(deviceId); + + if (capabilities != null && capabilities.SupportsPersistentIdentifier) + { + return false; + } + } + + return true; + } + + private bool CanAccessDevice(UserPolicy policy, string id) + { + if (policy.EnableAllDevices) + { + return true; + } + + if (policy.IsAdministrator) + { + return true; + } + + return ListHelper.ContainsIgnoreCase(policy.EnabledDevices, id); + } + } + + public class DevicesConfigStore : IConfigurationFactory + { + public IEnumerable<ConfigurationStore> GetConfigurations() + { + return new List<ConfigurationStore> + { + new ConfigurationStore + { + Key = "devices", + ConfigurationType = typeof(DevicesOptions) + } + }; + } + } + + public static class UploadConfigExtension + { + public static DevicesOptions GetUploadOptions(this IConfigurationManager config) + { + return config.GetConfiguration<DevicesOptions>("devices"); + } + } +}
\ No newline at end of file diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 11b3393c8..567c9b99e 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -52,6 +52,7 @@ <Compile Include="..\SharedVersion.cs"> <Link>Properties\SharedVersion.cs</Link> </Compile> + <Compile Include="Activity\ActivityLogEntryPoint.cs" /> <Compile Include="Activity\ActivityManager.cs" /> <Compile Include="Branding\BrandingConfigurationFactory.cs" /> <Compile Include="Channels\ChannelConfigurations.cs" /> @@ -60,7 +61,9 @@ <Compile Include="Channels\ChannelManager.cs" /> <Compile Include="Channels\ChannelPostScanTask.cs" /> <Compile Include="Channels\RefreshChannelsScheduledTask.cs" /> + <Compile Include="Collections\CollectionImageProvider.cs" /> <Compile Include="Collections\CollectionManager.cs" /> + <Compile Include="Devices\DeviceManager.cs" /> <Compile Include="Dto\DtoService.cs" /> <Compile Include="FileOrganization\EpisodeFileOrganizer.cs" /> <Compile Include="FileOrganization\Extensions.cs" /> @@ -69,6 +72,7 @@ <Compile Include="FileOrganization\NameUtils.cs" /> <Compile Include="FileOrganization\OrganizerScheduledTask.cs" /> <Compile Include="FileOrganization\TvFolderOrganizer.cs" /> + <Compile Include="Images\BaseDynamicImageProvider.cs" /> <Compile Include="Intros\DefaultIntroProvider.cs" /> <Compile Include="Library\CoreResolutionIgnoreRule.cs" /> <Compile Include="Library\LibraryManager.cs" /> @@ -94,6 +98,8 @@ <Compile Include="Library\Resolvers\TV\SeriesResolver.cs" /> <Compile Include="Library\Resolvers\VideoResolver.cs" /> <Compile Include="Library\SearchEngine.cs" /> + <Compile Include="Library\UserDataManager.cs" /> + <Compile Include="Library\UserManager.cs" /> <Compile Include="Library\UserViewManager.cs" /> <Compile Include="Library\Validators\ArtistsPostScanTask.cs" /> <Compile Include="Library\Validators\ArtistsValidator.cs" /> @@ -109,7 +115,11 @@ <Compile Include="Library\Validators\YearsPostScanTask.cs" /> <Compile Include="Logging\PatternsLogger.cs" /> <Compile Include="News\NewsService.cs" /> + <Compile Include="Notifications\Notifications.cs" /> + <Compile Include="Notifications\WebSocketNotifier.cs" /> <Compile Include="Persistence\CleanDatabaseScheduledTask.cs" /> + <Compile Include="Photos\PhotoAlbumImageProvider.cs" /> + <Compile Include="Playlists\PlaylistImageProvider.cs" /> <Compile Include="Playlists\PlaylistManager.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="ScheduledTasks\ChapterImagesTask.cs" /> @@ -149,7 +159,10 @@ <Compile Include="Sorting\SortNameComparer.cs" /> <Compile Include="Sorting\StartDateComparer.cs" /> <Compile Include="Sorting\StudioComparer.cs" /> + <Compile Include="TV\TVSeriesManager.cs" /> <Compile Include="Updates\InstallationManager.cs" /> + <Compile Include="UserViews\CollectionFolderImageProvider.cs" /> + <Compile Include="UserViews\DynamicImageProvider.cs" /> </ItemGroup> <ItemGroup> <Reference Include="MediaBrowser.Naming, Version=1.0.6146.28476, Culture=neutral, processorArchitecture=MSIL"> diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs new file mode 100644 index 000000000..224cd056a --- /dev/null +++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs @@ -0,0 +1,361 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.IO; +using MediaBrowser.Model.IO; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.Configuration; + +namespace Emby.Server.Implementations.Images +{ + public abstract class BaseDynamicImageProvider<T> : IHasItemChangeMonitor, IForcedProvider, ICustomMetadataProvider<T>, IHasOrder + where T : IHasMetadata + { + protected IFileSystem FileSystem { get; private set; } + protected IProviderManager ProviderManager { get; private set; } + protected IApplicationPaths ApplicationPaths { get; private set; } + protected IImageProcessor ImageProcessor { get; set; } + + protected BaseDynamicImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor) + { + ApplicationPaths = applicationPaths; + ProviderManager = providerManager; + FileSystem = fileSystem; + ImageProcessor = imageProcessor; + } + + protected virtual bool Supports(IHasImages item) + { + return true; + } + + public virtual IEnumerable<ImageType> GetSupportedImages(IHasImages item) + { + return new List<ImageType> + { + ImageType.Primary, + ImageType.Thumb + }; + } + + private IEnumerable<ImageType> GetEnabledImages(IHasImages item) + { + //var options = ProviderManager.GetMetadataOptions(item); + + return GetSupportedImages(item); + //return GetSupportedImages(item).Where(i => IsEnabled(options, i, item)).ToList(); + } + + private bool IsEnabled(MetadataOptions options, ImageType type, IHasImages item) + { + if (type == ImageType.Backdrop) + { + if (item.LockedFields.Contains(MetadataFields.Backdrops)) + { + return false; + } + } + else if (type == ImageType.Screenshot) + { + if (item.LockedFields.Contains(MetadataFields.Screenshots)) + { + return false; + } + } + else + { + if (item.LockedFields.Contains(MetadataFields.Images)) + { + return false; + } + } + + return options.IsEnabled(type); + } + + public async Task<ItemUpdateType> FetchAsync(T item, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + if (!Supports(item)) + { + return ItemUpdateType.None; + } + + var updateType = ItemUpdateType.None; + var supportedImages = GetEnabledImages(item).ToList(); + + if (supportedImages.Contains(ImageType.Primary)) + { + var primaryResult = await FetchAsync(item, ImageType.Primary, options, cancellationToken).ConfigureAwait(false); + updateType = updateType | primaryResult; + } + + if (supportedImages.Contains(ImageType.Thumb)) + { + var thumbResult = await FetchAsync(item, ImageType.Thumb, options, cancellationToken).ConfigureAwait(false); + updateType = updateType | thumbResult; + } + + return updateType; + } + + protected async Task<ItemUpdateType> FetchAsync(IHasImages item, ImageType imageType, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + var image = item.GetImageInfo(imageType, 0); + + if (image != null) + { + if (!image.IsLocalFile) + { + return ItemUpdateType.None; + } + + if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path)) + { + return ItemUpdateType.None; + } + } + + var items = await GetItemsWithImages(item).ConfigureAwait(false); + + return await FetchToFileInternal(item, items, imageType, cancellationToken).ConfigureAwait(false); + } + + protected async Task<ItemUpdateType> FetchToFileInternal(IHasImages item, + List<BaseItem> itemsWithImages, + ImageType imageType, + CancellationToken cancellationToken) + { + var outputPathWithoutExtension = Path.Combine(ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N")); + FileSystem.CreateDirectory(Path.GetDirectoryName(outputPathWithoutExtension)); + string outputPath = await CreateImage(item, itemsWithImages, outputPathWithoutExtension, imageType, 0).ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(outputPath)) + { + return ItemUpdateType.None; + } + + await ProviderManager.SaveImage(item, outputPath, "image/png", imageType, null, false, cancellationToken).ConfigureAwait(false); + + return ItemUpdateType.ImageUpdate; + } + + protected abstract Task<List<BaseItem>> GetItemsWithImages(IHasImages item); + + protected Task<string> CreateThumbCollage(IHasImages primaryItem, List<BaseItem> items, string outputPath) + { + return CreateCollage(primaryItem, items, outputPath, 640, 360); + } + + protected virtual IEnumerable<string> GetStripCollageImagePaths(IHasImages primaryItem, IEnumerable<BaseItem> items) + { + return items + .Select(i => + { + var image = i.GetImageInfo(ImageType.Primary, 0); + + if (image != null && image.IsLocalFile) + { + return image.Path; + } + image = i.GetImageInfo(ImageType.Thumb, 0); + + if (image != null && image.IsLocalFile) + { + return image.Path; + } + return null; + }) + .Where(i => !string.IsNullOrWhiteSpace(i)); + } + + protected Task<string> CreatePosterCollage(IHasImages primaryItem, List<BaseItem> items, string outputPath) + { + return CreateCollage(primaryItem, items, outputPath, 400, 600); + } + + protected Task<string> CreateSquareCollage(IHasImages primaryItem, List<BaseItem> items, string outputPath) + { + return CreateCollage(primaryItem, items, outputPath, 600, 600); + } + + protected Task<string> CreateThumbCollage(IHasImages primaryItem, List<BaseItem> items, string outputPath, int width, int height) + { + return CreateCollage(primaryItem, items, outputPath, width, height); + } + + private async Task<string> CreateCollage(IHasImages primaryItem, List<BaseItem> items, string outputPath, int width, int height) + { + FileSystem.CreateDirectory(Path.GetDirectoryName(outputPath)); + + var options = new ImageCollageOptions + { + Height = height, + Width = width, + OutputPath = outputPath, + InputPaths = GetStripCollageImagePaths(primaryItem, items).ToArray() + }; + + if (options.InputPaths.Length == 0) + { + return null; + } + + if (!ImageProcessor.SupportsImageCollageCreation) + { + return null; + } + + await ImageProcessor.CreateImageCollage(options).ConfigureAwait(false); + return outputPath; + } + + public string Name + { + get { return "Dynamic Image Provider"; } + } + + protected virtual async Task<string> CreateImage(IHasImages item, + List<BaseItem> itemsWithImages, + string outputPathWithoutExtension, + ImageType imageType, + int imageIndex) + { + if (itemsWithImages.Count == 0) + { + return null; + } + + string outputPath = Path.ChangeExtension(outputPathWithoutExtension, ".png"); + + if (imageType == ImageType.Thumb) + { + return await CreateThumbCollage(item, itemsWithImages, outputPath).ConfigureAwait(false); + } + + if (imageType == ImageType.Primary) + { + if (item is UserView) + { + return await CreateSquareCollage(item, itemsWithImages, outputPath).ConfigureAwait(false); + } + if (item is Playlist || item is MusicGenre) + { + return await CreateSquareCollage(item, itemsWithImages, outputPath).ConfigureAwait(false); + } + return await CreatePosterCollage(item, itemsWithImages, outputPath).ConfigureAwait(false); + } + + throw new ArgumentException("Unexpected image type"); + } + + protected virtual int MaxImageAgeDays + { + get { return 7; } + } + + public bool HasChanged(IHasMetadata item, IDirectoryService directoryServicee) + { + if (!Supports(item)) + { + return false; + } + + var supportedImages = GetEnabledImages(item).ToList(); + + if (supportedImages.Contains(ImageType.Primary) && HasChanged(item, ImageType.Primary)) + { + return true; + } + if (supportedImages.Contains(ImageType.Thumb) && HasChanged(item, ImageType.Thumb)) + { + return true; + } + + return false; + } + + protected bool HasChanged(IHasImages item, ImageType type) + { + var image = item.GetImageInfo(type, 0); + + if (image != null) + { + if (!image.IsLocalFile) + { + return false; + } + + if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path)) + { + return false; + } + + var age = DateTime.UtcNow - image.DateModified; + if (age.TotalDays <= MaxImageAgeDays) + { + return false; + } + } + + return true; + } + + protected List<BaseItem> GetFinalItems(List<BaseItem> items) + { + return GetFinalItems(items, 4); + } + + protected virtual List<BaseItem> GetFinalItems(List<BaseItem> items, int limit) + { + // Rotate the images once every x days + var random = DateTime.Now.DayOfYear % MaxImageAgeDays; + + return items + .OrderBy(i => (random + string.Empty + items.IndexOf(i)).GetMD5()) + .Take(limit) + .OrderBy(i => i.Name) + .ToList(); + } + + public int Order + { + get + { + // Run before the default image provider which will download placeholders + return 0; + } + } + + protected async Task<string> CreateSingleImage(List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType) + { + var image = itemsWithImages + .Where(i => i.HasImage(imageType) && i.GetImageInfo(imageType, 0).IsLocalFile && Path.HasExtension(i.GetImagePath(imageType))) + .Select(i => i.GetImagePath(imageType)) + .FirstOrDefault(); + + if (string.IsNullOrWhiteSpace(image)) + { + return null; + } + + var ext = Path.GetExtension(image); + + var outputPath = Path.ChangeExtension(outputPathWithoutExtension, ext); + FileSystem.CopyFile(image, outputPath, true); + + return outputPath; + } + } +} diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs new file mode 100644 index 000000000..c8dde1287 --- /dev/null +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -0,0 +1,287 @@ +using MediaBrowser.Common.Events; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Emby.Server.Implementations.Library +{ + /// <summary> + /// Class UserDataManager + /// </summary> + public class UserDataManager : IUserDataManager + { + public event EventHandler<UserDataSaveEventArgs> UserDataSaved; + + private readonly ConcurrentDictionary<string, UserItemData> _userData = + new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase); + + private readonly ILogger _logger; + private readonly IServerConfigurationManager _config; + + public UserDataManager(ILogManager logManager, IServerConfigurationManager config) + { + _config = config; + _logger = logManager.GetLogger(GetType().Name); + } + + /// <summary> + /// Gets or sets the repository. + /// </summary> + /// <value>The repository.</value> + public IUserDataRepository Repository { get; set; } + + public async Task SaveUserData(Guid userId, IHasUserData item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken) + { + if (userData == null) + { + throw new ArgumentNullException("userData"); + } + if (item == null) + { + throw new ArgumentNullException("item"); + } + if (userId == Guid.Empty) + { + throw new ArgumentNullException("userId"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + var keys = item.GetUserDataKeys(); + + foreach (var key in keys) + { + await Repository.SaveUserData(userId, key, userData, cancellationToken).ConfigureAwait(false); + } + + var cacheKey = GetCacheKey(userId, item.Id); + _userData.AddOrUpdate(cacheKey, userData, (k, v) => userData); + + EventHelper.FireEventIfNotNull(UserDataSaved, this, new UserDataSaveEventArgs + { + Keys = keys, + UserData = userData, + SaveReason = reason, + UserId = userId, + Item = item + + }, _logger); + } + + /// <summary> + /// Save the provided user data for the given user. Batch operation. Does not fire any events or update the cache. + /// </summary> + /// <param name="userId"></param> + /// <param name="userData"></param> + /// <param name="cancellationToken"></param> + /// <returns></returns> + public async Task SaveAllUserData(Guid userId, IEnumerable<UserItemData> userData, CancellationToken cancellationToken) + { + if (userData == null) + { + throw new ArgumentNullException("userData"); + } + if (userId == Guid.Empty) + { + throw new ArgumentNullException("userId"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + await Repository.SaveAllUserData(userId, userData, cancellationToken).ConfigureAwait(false); + } + + /// <summary> + /// Retrieve all user data for the given user + /// </summary> + /// <param name="userId"></param> + /// <returns></returns> + public IEnumerable<UserItemData> GetAllUserData(Guid userId) + { + if (userId == Guid.Empty) + { + throw new ArgumentNullException("userId"); + } + + return Repository.GetAllUserData(userId); + } + + public UserItemData GetUserData(Guid userId, Guid itemId, List<string> keys) + { + if (userId == Guid.Empty) + { + throw new ArgumentNullException("userId"); + } + if (keys == null) + { + throw new ArgumentNullException("keys"); + } + if (keys.Count == 0) + { + throw new ArgumentException("UserData keys cannot be empty."); + } + + var cacheKey = GetCacheKey(userId, itemId); + + return _userData.GetOrAdd(cacheKey, k => GetUserDataInternal(userId, keys)); + } + + private UserItemData GetUserDataInternal(Guid userId, List<string> keys) + { + var userData = Repository.GetUserData(userId, keys); + + if (userData != null) + { + return userData; + } + + if (keys.Count > 0) + { + return new UserItemData + { + UserId = userId, + Key = keys[0] + }; + } + + return null; + } + + /// <summary> + /// Gets the internal key. + /// </summary> + /// <returns>System.String.</returns> + private string GetCacheKey(Guid userId, Guid itemId) + { + return userId.ToString("N") + itemId.ToString("N"); + } + + public UserItemData GetUserData(IHasUserData user, IHasUserData item) + { + return GetUserData(user.Id, item); + } + + public UserItemData GetUserData(string userId, IHasUserData item) + { + return GetUserData(new Guid(userId), item); + } + + public UserItemData GetUserData(Guid userId, IHasUserData item) + { + return GetUserData(userId, item.Id, item.GetUserDataKeys()); + } + + public async Task<UserItemDataDto> GetUserDataDto(IHasUserData item, User user) + { + var userData = GetUserData(user.Id, item); + var dto = GetUserItemDataDto(userData); + + await item.FillUserDataDtoValues(dto, userData, null, user).ConfigureAwait(false); + return dto; + } + + public async Task<UserItemDataDto> GetUserDataDto(IHasUserData item, BaseItemDto itemDto, User user) + { + var userData = GetUserData(user.Id, item); + var dto = GetUserItemDataDto(userData); + + await item.FillUserDataDtoValues(dto, userData, itemDto, user).ConfigureAwait(false); + return dto; + } + + /// <summary> + /// Converts a UserItemData to a DTOUserItemData + /// </summary> + /// <param name="data">The data.</param> + /// <returns>DtoUserItemData.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + private UserItemDataDto GetUserItemDataDto(UserItemData data) + { + if (data == null) + { + throw new ArgumentNullException("data"); + } + + return new UserItemDataDto + { + IsFavorite = data.IsFavorite, + Likes = data.Likes, + PlaybackPositionTicks = data.PlaybackPositionTicks, + PlayCount = data.PlayCount, + Rating = data.Rating, + Played = data.Played, + LastPlayedDate = data.LastPlayedDate, + Key = data.Key + }; + } + + public bool UpdatePlayState(BaseItem item, UserItemData data, long? reportedPositionTicks) + { + var playedToCompletion = false; + + var positionTicks = reportedPositionTicks ?? item.RunTimeTicks ?? 0; + var hasRuntime = item.RunTimeTicks.HasValue && item.RunTimeTicks > 0; + + // If a position has been reported, and if we know the duration + if (positionTicks > 0 && hasRuntime) + { + var pctIn = Decimal.Divide(positionTicks, item.RunTimeTicks.Value) * 100; + + // Don't track in very beginning + if (pctIn < _config.Configuration.MinResumePct) + { + positionTicks = 0; + } + + // If we're at the end, assume completed + else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= item.RunTimeTicks.Value) + { + positionTicks = 0; + data.Played = playedToCompletion = true; + } + + else + { + // Enforce MinResumeDuration + var durationSeconds = TimeSpan.FromTicks(item.RunTimeTicks.Value).TotalSeconds; + + if (durationSeconds < _config.Configuration.MinResumeDurationSeconds) + { + positionTicks = 0; + data.Played = playedToCompletion = true; + } + } + } + else if (!hasRuntime) + { + // If we don't know the runtime we'll just have to assume it was fully played + data.Played = playedToCompletion = true; + positionTicks = 0; + } + + if (!item.SupportsPlayedStatus) + { + positionTicks = 0; + data.Played = false; + } + if (item is Audio) + { + positionTicks = 0; + } + + data.PlaybackPositionTicks = positionTicks; + + return playedToCompletion; + } + } +} diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs new file mode 100644 index 000000000..9c1d7fdf1 --- /dev/null +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -0,0 +1,1029 @@ +using MediaBrowser.Common.Events; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Connect; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Connect; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Users; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; + +namespace Emby.Server.Implementations.Library +{ + /// <summary> + /// Class UserManager + /// </summary> + public class UserManager : IUserManager + { + /// <summary> + /// Gets the users. + /// </summary> + /// <value>The users.</value> + public IEnumerable<User> Users { get; private set; } + + /// <summary> + /// The _logger + /// </summary> + private readonly ILogger _logger; + + /// <summary> + /// Gets or sets the configuration manager. + /// </summary> + /// <value>The configuration manager.</value> + private IServerConfigurationManager ConfigurationManager { get; set; } + + /// <summary> + /// Gets the active user repository + /// </summary> + /// <value>The user repository.</value> + private IUserRepository UserRepository { get; set; } + public event EventHandler<GenericEventArgs<User>> UserPasswordChanged; + + private readonly IXmlSerializer _xmlSerializer; + private readonly IJsonSerializer _jsonSerializer; + + private readonly INetworkManager _networkManager; + + private readonly Func<IImageProcessor> _imageProcessorFactory; + private readonly Func<IDtoService> _dtoServiceFactory; + private readonly Func<IConnectManager> _connectFactory; + private readonly IServerApplicationHost _appHost; + private readonly IFileSystem _fileSystem; + private readonly ICryptographyProvider _cryptographyProvider; + private readonly string _defaultUserName; + + public UserManager(ILogger logger, IServerConfigurationManager configurationManager, IUserRepository userRepository, IXmlSerializer xmlSerializer, INetworkManager networkManager, Func<IImageProcessor> imageProcessorFactory, Func<IDtoService> dtoServiceFactory, Func<IConnectManager> connectFactory, IServerApplicationHost appHost, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ICryptographyProvider cryptographyProvider, string defaultUserName) + { + _logger = logger; + UserRepository = userRepository; + _xmlSerializer = xmlSerializer; + _networkManager = networkManager; + _imageProcessorFactory = imageProcessorFactory; + _dtoServiceFactory = dtoServiceFactory; + _connectFactory = connectFactory; + _appHost = appHost; + _jsonSerializer = jsonSerializer; + _fileSystem = fileSystem; + _cryptographyProvider = cryptographyProvider; + _defaultUserName = defaultUserName; + ConfigurationManager = configurationManager; + Users = new List<User>(); + + DeletePinFile(); + } + + #region UserUpdated Event + /// <summary> + /// Occurs when [user updated]. + /// </summary> + public event EventHandler<GenericEventArgs<User>> UserUpdated; + public event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated; + public event EventHandler<GenericEventArgs<User>> UserLockedOut; + + /// <summary> + /// Called when [user updated]. + /// </summary> + /// <param name="user">The user.</param> + private void OnUserUpdated(User user) + { + EventHelper.FireEventIfNotNull(UserUpdated, this, new GenericEventArgs<User> { Argument = user }, _logger); + } + #endregion + + #region UserDeleted Event + /// <summary> + /// Occurs when [user deleted]. + /// </summary> + public event EventHandler<GenericEventArgs<User>> UserDeleted; + /// <summary> + /// Called when [user deleted]. + /// </summary> + /// <param name="user">The user.</param> + private void OnUserDeleted(User user) + { + EventHelper.QueueEventIfNotNull(UserDeleted, this, new GenericEventArgs<User> { Argument = user }, _logger); + } + #endregion + + /// <summary> + /// Gets a User by Id + /// </summary> + /// <param name="id">The id.</param> + /// <returns>User.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + public User GetUserById(Guid id) + { + if (id == Guid.Empty) + { + throw new ArgumentNullException("id"); + } + + return Users.FirstOrDefault(u => u.Id == id); + } + + /// <summary> + /// Gets the user by identifier. + /// </summary> + /// <param name="id">The identifier.</param> + /// <returns>User.</returns> + public User GetUserById(string id) + { + return GetUserById(new Guid(id)); + } + + public User GetUserByName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException("name"); + } + + return Users.FirstOrDefault(u => string.Equals(u.Name, name, StringComparison.OrdinalIgnoreCase)); + } + + public async Task Initialize() + { + Users = await LoadUsers().ConfigureAwait(false); + + var users = Users.ToList(); + + // If there are no local users with admin rights, make them all admins + if (!users.Any(i => i.Policy.IsAdministrator)) + { + foreach (var user in users) + { + if (!user.ConnectLinkType.HasValue || user.ConnectLinkType.Value == UserLinkType.LinkedUser) + { + user.Policy.IsAdministrator = true; + await UpdateUserPolicy(user, user.Policy, false).ConfigureAwait(false); + } + } + } + } + + public Task<bool> AuthenticateUser(string username, string passwordSha1, string remoteEndPoint) + { + return AuthenticateUser(username, passwordSha1, null, remoteEndPoint); + } + + public bool IsValidUsername(string username) + { + // Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) + foreach (var currentChar in username) + { + if (!IsValidUsernameCharacter(currentChar)) + { + return false; + } + } + return true; + } + + private bool IsValidUsernameCharacter(char i) + { + return char.IsLetterOrDigit(i) || char.Equals(i, '-') || char.Equals(i, '_') || char.Equals(i, '\'') || + char.Equals(i, '.'); + } + + public string MakeValidUsername(string username) + { + if (IsValidUsername(username)) + { + return username; + } + + // Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) + var builder = new StringBuilder(); + + foreach (var c in username) + { + if (IsValidUsernameCharacter(c)) + { + builder.Append(c); + } + } + return builder.ToString(); + } + + public async Task<bool> AuthenticateUser(string username, string passwordSha1, string passwordMd5, string remoteEndPoint) + { + if (string.IsNullOrWhiteSpace(username)) + { + throw new ArgumentNullException("username"); + } + + var user = Users + .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); + + if (user == null) + { + throw new SecurityException("Invalid username or password entered."); + } + + if (user.Policy.IsDisabled) + { + throw new SecurityException(string.Format("The {0} account is currently disabled. Please consult with your administrator.", user.Name)); + } + + var success = false; + + // Authenticate using local credentials if not a guest + if (!user.ConnectLinkType.HasValue || user.ConnectLinkType.Value != UserLinkType.Guest) + { + success = string.Equals(GetPasswordHash(user), passwordSha1.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + + if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword) + { + success = string.Equals(GetLocalPasswordHash(user), passwordSha1.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + } + } + + // Update LastActivityDate and LastLoginDate, then save + if (success) + { + user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; + await UpdateUser(user).ConfigureAwait(false); + await UpdateInvalidLoginAttemptCount(user, 0).ConfigureAwait(false); + } + else + { + await UpdateInvalidLoginAttemptCount(user, user.Policy.InvalidLoginAttemptCount + 1).ConfigureAwait(false); + } + + _logger.Info("Authentication request for {0} {1}.", user.Name, success ? "has succeeded" : "has been denied"); + + return success; + } + + private async Task UpdateInvalidLoginAttemptCount(User user, int newValue) + { + if (user.Policy.InvalidLoginAttemptCount != newValue || newValue > 0) + { + user.Policy.InvalidLoginAttemptCount = newValue; + + var maxCount = user.Policy.IsAdministrator ? + 3 : + 5; + + var fireLockout = false; + + if (newValue >= maxCount) + { + //_logger.Debug("Disabling user {0} due to {1} unsuccessful login attempts.", user.Name, newValue.ToString(CultureInfo.InvariantCulture)); + //user.Policy.IsDisabled = true; + + //fireLockout = true; + } + + await UpdateUserPolicy(user, user.Policy, false).ConfigureAwait(false); + + if (fireLockout) + { + if (UserLockedOut != null) + { + EventHelper.FireEventIfNotNull(UserLockedOut, this, new GenericEventArgs<User>(user), _logger); + } + } + } + } + + private string GetPasswordHash(User user) + { + return string.IsNullOrEmpty(user.Password) + ? GetSha1String(string.Empty) + : user.Password; + } + + private string GetLocalPasswordHash(User user) + { + return string.IsNullOrEmpty(user.EasyPassword) + ? GetSha1String(string.Empty) + : user.EasyPassword; + } + + private bool IsPasswordEmpty(string passwordHash) + { + return string.Equals(passwordHash, GetSha1String(string.Empty), StringComparison.OrdinalIgnoreCase); + } + + /// <summary> + /// Gets the sha1 string. + /// </summary> + /// <param name="str">The STR.</param> + /// <returns>System.String.</returns> + private string GetSha1String(string str) + { + return BitConverter.ToString(_cryptographyProvider.GetSHA1Bytes(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty); + } + + /// <summary> + /// Loads the users from the repository + /// </summary> + /// <returns>IEnumerable{User}.</returns> + private async Task<IEnumerable<User>> LoadUsers() + { + var users = UserRepository.RetrieveAllUsers().ToList(); + + // There always has to be at least one user. + if (users.Count == 0) + { + var name = MakeValidUsername(_defaultUserName); + + var user = InstantiateNewUser(name); + + user.DateLastSaved = DateTime.UtcNow; + + await UserRepository.SaveUser(user, CancellationToken.None).ConfigureAwait(false); + + users.Add(user); + + user.Policy.IsAdministrator = true; + user.Policy.EnableContentDeletion = true; + user.Policy.EnableRemoteControlOfOtherUsers = true; + await UpdateUserPolicy(user, user.Policy, false).ConfigureAwait(false); + } + + return users; + } + + public UserDto GetUserDto(User user, string remoteEndPoint = null) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + var passwordHash = GetPasswordHash(user); + + var hasConfiguredPassword = !IsPasswordEmpty(passwordHash); + var hasConfiguredEasyPassword = !IsPasswordEmpty(GetLocalPasswordHash(user)); + + var hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? + hasConfiguredEasyPassword : + hasConfiguredPassword; + + var dto = new UserDto + { + Id = user.Id.ToString("N"), + Name = user.Name, + HasPassword = hasPassword, + HasConfiguredPassword = hasConfiguredPassword, + HasConfiguredEasyPassword = hasConfiguredEasyPassword, + LastActivityDate = user.LastActivityDate, + LastLoginDate = user.LastLoginDate, + Configuration = user.Configuration, + ConnectLinkType = user.ConnectLinkType, + ConnectUserId = user.ConnectUserId, + ConnectUserName = user.ConnectUserName, + ServerId = _appHost.SystemId, + Policy = user.Policy + }; + + var image = user.GetImageInfo(ImageType.Primary, 0); + + if (image != null) + { + dto.PrimaryImageTag = GetImageCacheTag(user, image); + + try + { + _dtoServiceFactory().AttachPrimaryImageAspectRatio(dto, user); + } + catch (Exception ex) + { + // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions + _logger.ErrorException("Error generating PrimaryImageAspectRatio for {0}", ex, user.Name); + } + } + + return dto; + } + + public UserDto GetOfflineUserDto(User user) + { + var dto = GetUserDto(user); + + var offlinePasswordHash = GetLocalPasswordHash(user); + dto.HasPassword = !IsPasswordEmpty(offlinePasswordHash); + + dto.OfflinePasswordSalt = Guid.NewGuid().ToString("N"); + + // Hash the pin with the device Id to create a unique result for this device + dto.OfflinePassword = GetSha1String((offlinePasswordHash + dto.OfflinePasswordSalt).ToLower()); + + dto.ServerName = _appHost.FriendlyName; + + return dto; + } + + private string GetImageCacheTag(BaseItem item, ItemImageInfo image) + { + try + { + return _imageProcessorFactory().GetImageCacheTag(item, image); + } + catch (Exception ex) + { + _logger.ErrorException("Error getting {0} image info for {1}", ex, image.Type, image.Path); + return null; + } + } + + /// <summary> + /// Refreshes metadata for each user + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public Task RefreshUsersMetadata(CancellationToken cancellationToken) + { + var tasks = Users.Select(user => user.RefreshMetadata(new MetadataRefreshOptions(_fileSystem), cancellationToken)).ToList(); + + return Task.WhenAll(tasks); + } + + /// <summary> + /// Renames the user. + /// </summary> + /// <param name="user">The user.</param> + /// <param name="newName">The new name.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException">user</exception> + /// <exception cref="System.ArgumentException"></exception> + public async Task RenameUser(User user, string newName) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + if (string.IsNullOrEmpty(newName)) + { + throw new ArgumentNullException("newName"); + } + + if (Users.Any(u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", newName)); + } + + if (user.Name.Equals(newName, StringComparison.Ordinal)) + { + throw new ArgumentException("The new and old names must be different."); + } + + await user.Rename(newName); + + OnUserUpdated(user); + } + + /// <summary> + /// Updates the user. + /// </summary> + /// <param name="user">The user.</param> + /// <exception cref="System.ArgumentNullException">user</exception> + /// <exception cref="System.ArgumentException"></exception> + public async Task UpdateUser(User user) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + if (user.Id == Guid.Empty || !Users.Any(u => u.Id.Equals(user.Id))) + { + throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id)); + } + + user.DateModified = DateTime.UtcNow; + user.DateLastSaved = DateTime.UtcNow; + + await UserRepository.SaveUser(user, CancellationToken.None).ConfigureAwait(false); + + OnUserUpdated(user); + } + + public event EventHandler<GenericEventArgs<User>> UserCreated; + + private readonly SemaphoreSlim _userListLock = new SemaphoreSlim(1, 1); + + /// <summary> + /// Creates the user. + /// </summary> + /// <param name="name">The name.</param> + /// <returns>User.</returns> + /// <exception cref="System.ArgumentNullException">name</exception> + /// <exception cref="System.ArgumentException"></exception> + public async Task<User> CreateUser(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException("name"); + } + + if (!IsValidUsername(name)) + { + throw new ArgumentException("Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)"); + } + + if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", name)); + } + + await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); + + try + { + var user = InstantiateNewUser(name); + + var list = Users.ToList(); + list.Add(user); + Users = list; + + user.DateLastSaved = DateTime.UtcNow; + + await UserRepository.SaveUser(user, CancellationToken.None).ConfigureAwait(false); + + EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs<User> { Argument = user }, _logger); + + return user; + } + finally + { + _userListLock.Release(); + } + } + + /// <summary> + /// Deletes the user. + /// </summary> + /// <param name="user">The user.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException">user</exception> + /// <exception cref="System.ArgumentException"></exception> + public async Task DeleteUser(User user) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + if (user.ConnectLinkType.HasValue) + { + await _connectFactory().RemoveConnect(user.Id.ToString("N")).ConfigureAwait(false); + } + + var allUsers = Users.ToList(); + + if (allUsers.FirstOrDefault(u => u.Id == user.Id) == null) + { + throw new ArgumentException(string.Format("The user cannot be deleted because there is no user with the Name {0} and Id {1}.", user.Name, user.Id)); + } + + if (allUsers.Count == 1) + { + throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one user in the system.", user.Name)); + } + + if (user.Policy.IsAdministrator && allUsers.Count(i => i.Policy.IsAdministrator) == 1) + { + throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one admin user in the system.", user.Name)); + } + + await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); + + try + { + var configPath = GetConfigurationFilePath(user); + + await UserRepository.DeleteUser(user, CancellationToken.None).ConfigureAwait(false); + + try + { + _fileSystem.DeleteFile(configPath); + } + catch (IOException ex) + { + _logger.ErrorException("Error deleting file {0}", ex, configPath); + } + + DeleteUserPolicy(user); + + // Force this to be lazy loaded again + Users = await LoadUsers().ConfigureAwait(false); + + OnUserDeleted(user); + } + finally + { + _userListLock.Release(); + } + } + + /// <summary> + /// Resets the password by clearing it. + /// </summary> + /// <returns>Task.</returns> + public Task ResetPassword(User user) + { + return ChangePassword(user, GetSha1String(string.Empty)); + } + + public Task ResetEasyPassword(User user) + { + return ChangeEasyPassword(user, GetSha1String(string.Empty)); + } + + public async Task ChangePassword(User user, string newPasswordSha1) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + if (string.IsNullOrWhiteSpace(newPasswordSha1)) + { + throw new ArgumentNullException("newPasswordSha1"); + } + + if (user.ConnectLinkType.HasValue && user.ConnectLinkType.Value == UserLinkType.Guest) + { + throw new ArgumentException("Passwords for guests cannot be changed."); + } + + user.Password = newPasswordSha1; + + await UpdateUser(user).ConfigureAwait(false); + + EventHelper.FireEventIfNotNull(UserPasswordChanged, this, new GenericEventArgs<User>(user), _logger); + } + + public async Task ChangeEasyPassword(User user, string newPasswordSha1) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + if (string.IsNullOrWhiteSpace(newPasswordSha1)) + { + throw new ArgumentNullException("newPasswordSha1"); + } + + user.EasyPassword = newPasswordSha1; + + await UpdateUser(user).ConfigureAwait(false); + + EventHelper.FireEventIfNotNull(UserPasswordChanged, this, new GenericEventArgs<User>(user), _logger); + } + + /// <summary> + /// Instantiates the new user. + /// </summary> + /// <param name="name">The name.</param> + /// <returns>User.</returns> + private User InstantiateNewUser(string name) + { + return new User + { + Name = name, + Id = Guid.NewGuid(), + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow, + UsesIdForConfigurationPath = true + }; + } + + private string PasswordResetFile + { + get { return Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "passwordreset.txt"); } + } + + private string _lastPin; + private PasswordPinCreationResult _lastPasswordPinCreationResult; + private int _pinAttempts; + + private PasswordPinCreationResult CreatePasswordResetPin() + { + var num = new Random().Next(1, 9999); + + var path = PasswordResetFile; + + var pin = num.ToString("0000", CultureInfo.InvariantCulture); + _lastPin = pin; + + var time = TimeSpan.FromMinutes(5); + var expiration = DateTime.UtcNow.Add(time); + + var text = new StringBuilder(); + + var localAddress = _appHost.GetLocalApiUrl().Result ?? string.Empty; + + text.AppendLine("Use your web browser to visit:"); + text.AppendLine(string.Empty); + text.AppendLine(localAddress + "/web/forgotpasswordpin.html"); + text.AppendLine(string.Empty); + text.AppendLine("Enter the following pin code:"); + text.AppendLine(string.Empty); + text.AppendLine(pin); + text.AppendLine(string.Empty); + + var localExpirationTime = expiration.ToLocalTime(); + // Tuesday, 22 August 2006 06:30 AM + text.AppendLine("The pin code will expire at " + localExpirationTime.ToString("f1", CultureInfo.CurrentCulture)); + + _fileSystem.WriteAllText(path, text.ToString(), Encoding.UTF8); + + var result = new PasswordPinCreationResult + { + PinFile = path, + ExpirationDate = expiration + }; + + _lastPasswordPinCreationResult = result; + _pinAttempts = 0; + + return result; + } + + public ForgotPasswordResult StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) + { + DeletePinFile(); + + var user = string.IsNullOrWhiteSpace(enteredUsername) ? + null : + GetUserByName(enteredUsername); + + if (user != null && user.ConnectLinkType.HasValue && user.ConnectLinkType.Value == UserLinkType.Guest) + { + throw new ArgumentException("Unable to process forgot password request for guests."); + } + + var action = ForgotPasswordAction.InNetworkRequired; + string pinFile = null; + DateTime? expirationDate = null; + + if (user != null && !user.Policy.IsAdministrator) + { + action = ForgotPasswordAction.ContactAdmin; + } + else + { + if (isInNetwork) + { + action = ForgotPasswordAction.PinCode; + } + + var result = CreatePasswordResetPin(); + pinFile = result.PinFile; + expirationDate = result.ExpirationDate; + } + + return new ForgotPasswordResult + { + Action = action, + PinFile = pinFile, + PinExpirationDate = expirationDate + }; + } + + public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin) + { + DeletePinFile(); + + var usersReset = new List<string>(); + + var valid = !string.IsNullOrWhiteSpace(_lastPin) && + string.Equals(_lastPin, pin, StringComparison.OrdinalIgnoreCase) && + _lastPasswordPinCreationResult != null && + _lastPasswordPinCreationResult.ExpirationDate > DateTime.UtcNow; + + if (valid) + { + _lastPin = null; + _lastPasswordPinCreationResult = null; + + var users = Users.Where(i => !i.ConnectLinkType.HasValue || i.ConnectLinkType.Value != UserLinkType.Guest) + .ToList(); + + foreach (var user in users) + { + await ResetPassword(user).ConfigureAwait(false); + + if (user.Policy.IsDisabled) + { + user.Policy.IsDisabled = false; + await UpdateUserPolicy(user, user.Policy, true).ConfigureAwait(false); + } + usersReset.Add(user.Name); + } + } + else + { + _pinAttempts++; + if (_pinAttempts >= 3) + { + _lastPin = null; + _lastPasswordPinCreationResult = null; + } + } + + return new PinRedeemResult + { + Success = valid, + UsersReset = usersReset.ToArray() + }; + } + + private void DeletePinFile() + { + try + { + _fileSystem.DeleteFile(PasswordResetFile); + } + catch + { + + } + } + + class PasswordPinCreationResult + { + public string PinFile { get; set; } + public DateTime ExpirationDate { get; set; } + } + + public UserPolicy GetUserPolicy(User user) + { + var path = GetPolifyFilePath(user); + + try + { + lock (_policySyncLock) + { + return (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), path); + } + } + catch (FileNotFoundException) + { + return GetDefaultPolicy(user); + } + catch (IOException) + { + return GetDefaultPolicy(user); + } + catch (Exception ex) + { + _logger.ErrorException("Error reading policy file: {0}", ex, path); + + return GetDefaultPolicy(user); + } + } + + private UserPolicy GetDefaultPolicy(User user) + { + return new UserPolicy + { + EnableSync = true + }; + } + + private readonly object _policySyncLock = new object(); + public Task UpdateUserPolicy(string userId, UserPolicy userPolicy) + { + var user = GetUserById(userId); + return UpdateUserPolicy(user, userPolicy, true); + } + + private async Task UpdateUserPolicy(User user, UserPolicy userPolicy, bool fireEvent) + { + // The xml serializer will output differently if the type is not exact + if (userPolicy.GetType() != typeof(UserPolicy)) + { + var json = _jsonSerializer.SerializeToString(userPolicy); + userPolicy = _jsonSerializer.DeserializeFromString<UserPolicy>(json); + } + + var path = GetPolifyFilePath(user); + + _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); + + lock (_policySyncLock) + { + _xmlSerializer.SerializeToFile(userPolicy, path); + user.Policy = userPolicy; + } + + await UpdateConfiguration(user, user.Configuration, true).ConfigureAwait(false); + } + + private void DeleteUserPolicy(User user) + { + var path = GetPolifyFilePath(user); + + try + { + lock (_policySyncLock) + { + _fileSystem.DeleteFile(path); + } + } + catch (IOException) + { + + } + catch (Exception ex) + { + _logger.ErrorException("Error deleting policy file", ex); + } + } + + private string GetPolifyFilePath(User user) + { + return Path.Combine(user.ConfigurationDirectoryPath, "policy.xml"); + } + + private string GetConfigurationFilePath(User user) + { + return Path.Combine(user.ConfigurationDirectoryPath, "config.xml"); + } + + public UserConfiguration GetUserConfiguration(User user) + { + var path = GetConfigurationFilePath(user); + + try + { + lock (_configSyncLock) + { + return (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), path); + } + } + catch (FileNotFoundException) + { + return new UserConfiguration(); + } + catch (IOException) + { + return new UserConfiguration(); + } + catch (Exception ex) + { + _logger.ErrorException("Error reading policy file: {0}", ex, path); + + return new UserConfiguration(); + } + } + + private readonly object _configSyncLock = new object(); + public Task UpdateConfiguration(string userId, UserConfiguration config) + { + var user = GetUserById(userId); + return UpdateConfiguration(user, config, true); + } + + private async Task UpdateConfiguration(User user, UserConfiguration config, bool fireEvent) + { + var path = GetConfigurationFilePath(user); + + // The xml serializer will output differently if the type is not exact + if (config.GetType() != typeof(UserConfiguration)) + { + var json = _jsonSerializer.SerializeToString(config); + config = _jsonSerializer.DeserializeFromString<UserConfiguration>(json); + } + + _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); + + lock (_configSyncLock) + { + _xmlSerializer.SerializeToFile(config, path); + user.Configuration = config; + } + + if (fireEvent) + { + EventHelper.FireEventIfNotNull(UserConfigurationUpdated, this, new GenericEventArgs<User> { Argument = user }, _logger); + } + } + } +}
\ No newline at end of file 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; + } + } +} diff --git a/Emby.Server.Implementations/Photos/PhotoAlbumImageProvider.cs b/Emby.Server.Implementations/Photos/PhotoAlbumImageProvider.cs new file mode 100644 index 000000000..cc1756f96 --- /dev/null +++ b/Emby.Server.Implementations/Photos/PhotoAlbumImageProvider.cs @@ -0,0 +1,34 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Emby.Server.Implementations.Images; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Entities; + +namespace Emby.Server.Implementations.Photos +{ + public class PhotoAlbumImageProvider : BaseDynamicImageProvider<PhotoAlbum> + { + public PhotoAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor) + : base(fileSystem, providerManager, applicationPaths, imageProcessor) + { + } + + protected override Task<List<BaseItem>> GetItemsWithImages(IHasImages item) + { + var photoAlbum = (PhotoAlbum)item; + var items = GetFinalItems(photoAlbum.Children.ToList()); + + return Task.FromResult(items); + } + + protected override Task<string> CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + { + return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary); + } + } +} diff --git a/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs b/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs new file mode 100644 index 000000000..ef7d6dba8 --- /dev/null +++ b/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs @@ -0,0 +1,104 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Emby.Server.Implementations.Images; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Querying; + +namespace Emby.Server.Implementations.Playlists +{ + public class PlaylistImageProvider : BaseDynamicImageProvider<Playlist> + { + public PlaylistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor) : base(fileSystem, providerManager, applicationPaths, imageProcessor) + { + } + + protected override Task<List<BaseItem>> GetItemsWithImages(IHasImages item) + { + var playlist = (Playlist)item; + + var items = playlist.GetManageableItems() + .Select(i => + { + var subItem = i.Item2; + + var episode = subItem as Episode; + + if (episode != null) + { + var series = episode.Series; + if (series != null && series.HasImage(ImageType.Primary)) + { + return series; + } + } + + if (subItem.HasImage(ImageType.Primary)) + { + return subItem; + } + + var parent = subItem.GetParent(); + + if (parent != null && parent.HasImage(ImageType.Primary)) + { + if (parent is MusicAlbum) + { + return parent; + } + } + + return null; + }) + .Where(i => i != null) + .DistinctBy(i => i.Id) + .ToList(); + + return Task.FromResult(GetFinalItems(items)); + } + } + + public class MusicGenreImageProvider : BaseDynamicImageProvider<MusicGenre> + { + private readonly ILibraryManager _libraryManager; + + public MusicGenreImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor) + { + _libraryManager = libraryManager; + } + + protected override Task<List<BaseItem>> GetItemsWithImages(IHasImages item) + { + var items = _libraryManager.GetItemList(new InternalItemsQuery + { + Genres = new[] { item.Name }, + IncludeItemTypes = new[] { typeof(MusicAlbum).Name, typeof(MusicVideo).Name, typeof(Audio).Name }, + SortBy = new[] { ItemSortBy.Random }, + Limit = 4, + Recursive = true, + ImageTypes = new[] { ImageType.Primary } + + }).ToList(); + + return Task.FromResult(GetFinalItems(items)); + } + + //protected override Task<string> CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + //{ + // return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary); + //} + } + +} diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs new file mode 100644 index 000000000..f3bab7883 --- /dev/null +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -0,0 +1,226 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.TV; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Controller.Configuration; + +namespace Emby.Server.Implementations.TV +{ + public class TVSeriesManager : ITVSeriesManager + { + private readonly IUserManager _userManager; + private readonly IUserDataManager _userDataManager; + private readonly ILibraryManager _libraryManager; + private readonly IServerConfigurationManager _config; + + public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager, IServerConfigurationManager config) + { + _userManager = userManager; + _userDataManager = userDataManager; + _libraryManager = libraryManager; + _config = config; + } + + public QueryResult<BaseItem> GetNextUp(NextUpQuery request) + { + var user = _userManager.GetUserById(request.UserId); + + if (user == null) + { + throw new ArgumentException("User not found"); + } + + var parentIdGuid = string.IsNullOrWhiteSpace(request.ParentId) ? (Guid?)null : new Guid(request.ParentId); + + string presentationUniqueKey = null; + int? limit = null; + if (!string.IsNullOrWhiteSpace(request.SeriesId)) + { + var series = _libraryManager.GetItemById(request.SeriesId); + + if (series != null) + { + presentationUniqueKey = GetUniqueSeriesKey(series); + limit = 1; + } + } + + if (string.IsNullOrWhiteSpace(presentationUniqueKey) && limit.HasValue) + { + limit = limit.Value + 10; + } + + var items = _libraryManager.GetItemList(new InternalItemsQuery(user) + { + IncludeItemTypes = new[] { typeof(Series).Name }, + SortOrder = SortOrder.Ascending, + PresentationUniqueKey = presentationUniqueKey, + Limit = limit, + ParentId = parentIdGuid, + Recursive = true + + }).Cast<Series>(); + + // Avoid implicitly captured closure + var episodes = GetNextUpEpisodes(request, user, items); + + return GetResult(episodes, null, request); + } + + public QueryResult<BaseItem> GetNextUp(NextUpQuery request, IEnumerable<Folder> parentsFolders) + { + var user = _userManager.GetUserById(request.UserId); + + if (user == null) + { + throw new ArgumentException("User not found"); + } + + string presentationUniqueKey = null; + int? limit = null; + if (!string.IsNullOrWhiteSpace(request.SeriesId)) + { + var series = _libraryManager.GetItemById(request.SeriesId); + + if (series != null) + { + presentationUniqueKey = GetUniqueSeriesKey(series); + limit = 1; + } + } + + if (string.IsNullOrWhiteSpace(presentationUniqueKey) && limit.HasValue) + { + limit = limit.Value + 10; + } + + var items = _libraryManager.GetItemList(new InternalItemsQuery(user) + { + IncludeItemTypes = new[] { typeof(Series).Name }, + SortOrder = SortOrder.Ascending, + PresentationUniqueKey = presentationUniqueKey, + Limit = limit + + }, parentsFolders.Select(i => i.Id.ToString("N"))).Cast<Series>(); + + // Avoid implicitly captured closure + var episodes = GetNextUpEpisodes(request, user, items); + + return GetResult(episodes, null, request); + } + + public IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IEnumerable<Series> series) + { + // Avoid implicitly captured closure + var currentUser = user; + + var allNextUp = series + .Select(i => GetNextUp(i, currentUser)) + .Where(i => i.Item1 != null) + // Include if an episode was found, and either the series is not unwatched or the specific series was requested + .OrderByDescending(i => i.Item2) + .ThenByDescending(i => i.Item1.PremiereDate ?? DateTime.MinValue) + .ToList(); + + // If viewing all next up for all series, remove first episodes + if (string.IsNullOrWhiteSpace(request.SeriesId)) + { + var withoutFirstEpisode = allNextUp + .Where(i => !i.Item3) + .ToList(); + + // But if that returns empty, keep those first episodes (avoid completely empty view) + if (withoutFirstEpisode.Count > 0) + { + allNextUp = withoutFirstEpisode; + } + } + + return allNextUp + .Select(i => i.Item1) + .Take(request.Limit ?? int.MaxValue); + } + + private string GetUniqueSeriesKey(BaseItem series) + { + if (_config.Configuration.SchemaVersion < 97) + { + return series.Id.ToString("N"); + } + return series.GetPresentationUniqueKey(); + } + + /// <summary> + /// Gets the next up. + /// </summary> + /// <param name="series">The series.</param> + /// <param name="user">The user.</param> + /// <returns>Task{Episode}.</returns> + private Tuple<Episode, DateTime, bool> GetNextUp(Series series, User user) + { + var lastWatchedEpisode = _libraryManager.GetItemList(new InternalItemsQuery(user) + { + AncestorWithPresentationUniqueKey = GetUniqueSeriesKey(series), + IncludeItemTypes = new[] { typeof(Episode).Name }, + SortBy = new[] { ItemSortBy.SortName }, + SortOrder = SortOrder.Descending, + IsPlayed = true, + Limit = 1, + ParentIndexNumberNotEquals = 0 + + }).FirstOrDefault(); + + var firstUnwatchedEpisode = _libraryManager.GetItemList(new InternalItemsQuery(user) + { + AncestorWithPresentationUniqueKey = GetUniqueSeriesKey(series), + IncludeItemTypes = new[] { typeof(Episode).Name }, + SortBy = new[] { ItemSortBy.SortName }, + SortOrder = SortOrder.Ascending, + Limit = 1, + IsPlayed = false, + IsVirtualItem = false, + ParentIndexNumberNotEquals = 0, + MinSortName = lastWatchedEpisode == null ? null : lastWatchedEpisode.SortName + + }).Cast<Episode>().FirstOrDefault(); + + if (lastWatchedEpisode != null && firstUnwatchedEpisode != null) + { + var userData = _userDataManager.GetUserData(user, lastWatchedEpisode); + + var lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue.AddDays(1); + + return new Tuple<Episode, DateTime, bool>(firstUnwatchedEpisode, lastWatchedDate, false); + } + + // Return the first episode + return new Tuple<Episode, DateTime, bool>(firstUnwatchedEpisode, DateTime.MinValue, true); + } + + private QueryResult<BaseItem> GetResult(IEnumerable<BaseItem> items, int? totalRecordLimit, NextUpQuery query) + { + var itemsArray = totalRecordLimit.HasValue ? items.Take(totalRecordLimit.Value).ToArray() : items.ToArray(); + var totalCount = itemsArray.Length; + + if (query.Limit.HasValue) + { + itemsArray = itemsArray.Skip(query.StartIndex ?? 0).Take(query.Limit.Value).ToArray(); + } + else if (query.StartIndex.HasValue) + { + itemsArray = itemsArray.Skip(query.StartIndex.Value).ToArray(); + } + + return new QueryResult<BaseItem> + { + TotalRecordCount = totalCount, + Items = itemsArray + }; + } + } +} diff --git a/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs new file mode 100644 index 000000000..ab6307238 --- /dev/null +++ b/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs @@ -0,0 +1,176 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Emby.Server.Implementations.Images; +using MediaBrowser.Common.IO; +using MediaBrowser.Model.IO; +using MediaBrowser.Controller.Collections; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Querying; + +namespace Emby.Server.Implementations.UserViews +{ + public class CollectionFolderImageProvider : BaseDynamicImageProvider<CollectionFolder> + { + public CollectionFolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor) : base(fileSystem, providerManager, applicationPaths, imageProcessor) + { + } + + public override IEnumerable<ImageType> GetSupportedImages(IHasImages item) + { + return new List<ImageType> + { + ImageType.Primary + }; + } + + protected override async Task<List<BaseItem>> GetItemsWithImages(IHasImages item) + { + var view = (CollectionFolder)item; + + var recursive = !new[] { CollectionType.Playlists, CollectionType.Channels }.Contains(view.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + + var result = await view.GetItems(new InternalItemsQuery + { + CollapseBoxSetItems = false, + Recursive = recursive, + ExcludeItemTypes = new[] { "UserView", "CollectionFolder", "Playlist" } + + }).ConfigureAwait(false); + + var items = result.Items.Select(i => + { + var episode = i as Episode; + if (episode != null) + { + var series = episode.Series; + if (series != null) + { + return series; + } + + return episode; + } + + var season = i as Season; + if (season != null) + { + var series = season.Series; + if (series != null) + { + return series; + } + + return season; + } + + var audio = i as Audio; + if (audio != null) + { + var album = audio.AlbumEntity; + if (album != null && album.HasImage(ImageType.Primary)) + { + return album; + } + } + + return i; + + }).DistinctBy(i => i.Id); + + return GetFinalItems(items.Where(i => i.HasImage(ImageType.Primary) || i.HasImage(ImageType.Thumb)).ToList(), 8); + } + + protected override bool Supports(IHasImages item) + { + return item is CollectionFolder; + } + + protected override async Task<string> CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + { + var outputPath = Path.ChangeExtension(outputPathWithoutExtension, ".png"); + + if (imageType == ImageType.Primary) + { + if (itemsWithImages.Count == 0) + { + return null; + } + + return await CreateThumbCollage(item, itemsWithImages, outputPath, 960, 540).ConfigureAwait(false); + } + + return await base.CreateImage(item, itemsWithImages, outputPath, imageType, imageIndex).ConfigureAwait(false); + } + } + + public class ManualCollectionFolderImageProvider : BaseDynamicImageProvider<ManualCollectionsFolder> + { + private readonly ILibraryManager _libraryManager; + + public ManualCollectionFolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor) + { + _libraryManager = libraryManager; + } + + public override IEnumerable<ImageType> GetSupportedImages(IHasImages item) + { + return new List<ImageType> + { + ImageType.Primary + }; + } + + protected override async Task<List<BaseItem>> GetItemsWithImages(IHasImages item) + { + var view = (ManualCollectionsFolder)item; + + var recursive = !new[] { CollectionType.Playlists, CollectionType.Channels }.Contains(view.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + + var items = _libraryManager.GetItemList(new InternalItemsQuery + { + Recursive = recursive, + IncludeItemTypes = new[] { typeof(BoxSet).Name }, + Limit = 20, + SortBy = new[] { ItemSortBy.Random } + }); + + return GetFinalItems(items.Where(i => i.HasImage(ImageType.Primary) || i.HasImage(ImageType.Thumb)).ToList(), 8); + } + + protected override bool Supports(IHasImages item) + { + return item is ManualCollectionsFolder; + } + + protected override async Task<string> CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + { + var outputPath = Path.ChangeExtension(outputPathWithoutExtension, ".png"); + + if (imageType == ImageType.Primary) + { + if (itemsWithImages.Count == 0) + { + return null; + } + + return await CreateThumbCollage(item, itemsWithImages, outputPath, 960, 540).ConfigureAwait(false); + } + + return await base.CreateImage(item, itemsWithImages, outputPath, imageType, imageIndex).ConfigureAwait(false); + } + } + +} diff --git a/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs b/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs new file mode 100644 index 000000000..09b68c8ea --- /dev/null +++ b/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs @@ -0,0 +1,188 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Emby.Server.Implementations.Images; +using MediaBrowser.Model.IO; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Extensions; + +namespace Emby.Server.Implementations.UserViews +{ + public class DynamicImageProvider : BaseDynamicImageProvider<UserView> + { + private readonly IUserManager _userManager; + private readonly ILibraryManager _libraryManager; + + public DynamicImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, IUserManager userManager, ILibraryManager libraryManager) + : base(fileSystem, providerManager, applicationPaths, imageProcessor) + { + _userManager = userManager; + _libraryManager = libraryManager; + } + + public override IEnumerable<ImageType> GetSupportedImages(IHasImages item) + { + var view = (UserView)item; + if (IsUsingCollectionStrip(view)) + { + return new List<ImageType> + { + ImageType.Primary + }; + } + + return new List<ImageType> + { + ImageType.Primary + }; + } + + protected override async Task<List<BaseItem>> GetItemsWithImages(IHasImages item) + { + var view = (UserView)item; + + if (string.Equals(view.ViewType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase)) + { + var programs = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, + ImageTypes = new[] { ImageType.Primary }, + Limit = 30, + IsMovie = true + }).ToList(); + + return GetFinalItems(programs).ToList(); + } + + if (string.Equals(view.ViewType, SpecialFolder.MovieGenre, StringComparison.OrdinalIgnoreCase) || + string.Equals(view.ViewType, SpecialFolder.TvGenre, StringComparison.OrdinalIgnoreCase)) + { + var userItemsResult = await view.GetItems(new InternalItemsQuery + { + CollapseBoxSetItems = false + }); + + return userItemsResult.Items.ToList(); + } + + var isUsingCollectionStrip = IsUsingCollectionStrip(view); + var recursive = isUsingCollectionStrip && !new[] { CollectionType.Channels, CollectionType.BoxSets, CollectionType.Playlists }.Contains(view.ViewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + + var result = await view.GetItems(new InternalItemsQuery + { + User = view.UserId.HasValue ? _userManager.GetUserById(view.UserId.Value) : null, + CollapseBoxSetItems = false, + Recursive = recursive, + ExcludeItemTypes = new[] { "UserView", "CollectionFolder", "Person" }, + + }).ConfigureAwait(false); + + var items = result.Items.Select(i => + { + var episode = i as Episode; + if (episode != null) + { + var series = episode.Series; + if (series != null) + { + return series; + } + + return episode; + } + + var season = i as Season; + if (season != null) + { + var series = season.Series; + if (series != null) + { + return series; + } + + return season; + } + + var audio = i as Audio; + if (audio != null) + { + var album = audio.AlbumEntity; + if (album != null && album.HasImage(ImageType.Primary)) + { + return album; + } + } + + return i; + + }).DistinctBy(i => i.Id); + + if (isUsingCollectionStrip) + { + return GetFinalItems(items.Where(i => i.HasImage(ImageType.Primary) || i.HasImage(ImageType.Thumb)).ToList(), 8); + } + + return GetFinalItems(items.Where(i => i.HasImage(ImageType.Primary)).ToList()); + } + + protected override bool Supports(IHasImages item) + { + var view = item as UserView; + if (view != null) + { + return IsUsingCollectionStrip(view); + } + + return false; + } + + private bool IsUsingCollectionStrip(UserView view) + { + string[] collectionStripViewTypes = + { + CollectionType.Movies, + CollectionType.TvShows, + CollectionType.Music, + CollectionType.Games, + CollectionType.Books, + CollectionType.MusicVideos, + CollectionType.HomeVideos, + CollectionType.BoxSets, + CollectionType.LiveTv, + CollectionType.Playlists, + CollectionType.Photos, + string.Empty + }; + + return collectionStripViewTypes.Contains(view.ViewType ?? string.Empty); + } + + protected override async Task<string> CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + { + var outputPath = Path.ChangeExtension(outputPathWithoutExtension, ".png"); + + var view = (UserView)item; + if (imageType == ImageType.Primary && IsUsingCollectionStrip(view)) + { + if (itemsWithImages.Count == 0) + { + return null; + } + + return await CreateThumbCollage(item, itemsWithImages, outputPath, 960, 540).ConfigureAwait(false); + } + + return await base.CreateImage(item, itemsWithImages, outputPath, imageType, imageIndex).ConfigureAwait(false); + } + } +} |
