aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations')
-rw-r--r--Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs561
-rw-r--r--Emby.Server.Implementations/Collections/CollectionImageProvider.cs84
-rw-r--r--Emby.Server.Implementations/Devices/DeviceManager.cs306
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj13
-rw-r--r--Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs361
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs287
-rw-r--r--Emby.Server.Implementations/Library/UserManager.cs1029
-rw-r--r--Emby.Server.Implementations/Notifications/Notifications.cs547
-rw-r--r--Emby.Server.Implementations/Notifications/WebSocketNotifier.cs54
-rw-r--r--Emby.Server.Implementations/Photos/PhotoAlbumImageProvider.cs34
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs104
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs226
-rw-r--r--Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs176
-rw-r--r--Emby.Server.Implementations/UserViews/DynamicImageProvider.cs188
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);
+ }
+ }
+}