From 41bef184d1036b02baec00734f3edd8abbebf5fe Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Wed, 2 Nov 2016 17:05:17 -0400 Subject: move classes to portable server project --- .../Collections/CollectionManager.cs | 296 +++++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 Emby.Server.Implementations/Collections/CollectionManager.cs (limited to 'Emby.Server.Implementations/Collections') diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs new file mode 100644 index 0000000000..d0bd76c354 --- /dev/null +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -0,0 +1,296 @@ +using MediaBrowser.Common.Events; +using MediaBrowser.Controller.Collections; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.IO; + +namespace Emby.Server.Implementations.Collections +{ + public class CollectionManager : ICollectionManager + { + private readonly ILibraryManager _libraryManager; + private readonly IFileSystem _fileSystem; + private readonly ILibraryMonitor _iLibraryMonitor; + private readonly ILogger _logger; + private readonly IProviderManager _providerManager; + + public event EventHandler CollectionCreated; + public event EventHandler ItemsAddedToCollection; + public event EventHandler ItemsRemovedFromCollection; + + public CollectionManager(ILibraryManager libraryManager, IFileSystem fileSystem, ILibraryMonitor iLibraryMonitor, ILogger logger, IProviderManager providerManager) + { + _libraryManager = libraryManager; + _fileSystem = fileSystem; + _iLibraryMonitor = iLibraryMonitor; + _logger = logger; + _providerManager = providerManager; + } + + public Folder GetCollectionsFolder(string userId) + { + return _libraryManager.RootFolder.Children.OfType() + .FirstOrDefault() ?? _libraryManager.GetUserRootFolder().Children.OfType() + .FirstOrDefault(); + } + + public IEnumerable GetCollections(User user) + { + var folder = GetCollectionsFolder(user.Id.ToString("N")); + return folder == null ? + new List() : + folder.GetChildren(user, true).OfType(); + } + + public async Task CreateCollection(CollectionCreationOptions options) + { + var name = options.Name; + + // Need to use the [boxset] suffix + // If internet metadata is not found, or if xml saving is off there will be no collection.xml + // This could cause it to get re-resolved as a plain folder + var folderName = _fileSystem.GetValidFilename(name) + " [boxset]"; + + var parentFolder = GetParentFolder(options.ParentId); + + if (parentFolder == null) + { + throw new ArgumentException(); + } + + var path = Path.Combine(parentFolder.Path, folderName); + + _iLibraryMonitor.ReportFileSystemChangeBeginning(path); + + try + { + _fileSystem.CreateDirectory(path); + + var collection = new BoxSet + { + Name = name, + Path = path, + IsLocked = options.IsLocked, + ProviderIds = options.ProviderIds, + Shares = options.UserIds.Select(i => new Share + { + UserId = i.ToString("N"), + CanEdit = true + + }).ToList() + }; + + await parentFolder.AddChild(collection, CancellationToken.None).ConfigureAwait(false); + + if (options.ItemIdList.Count > 0) + { + await AddToCollection(collection.Id, options.ItemIdList, false, new MetadataRefreshOptions(_fileSystem) + { + // The initial adding of items is going to create a local metadata file + // This will cause internet metadata to be skipped as a result + MetadataRefreshMode = MetadataRefreshMode.FullRefresh + }); + } + else + { + _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(_fileSystem)); + } + + EventHelper.FireEventIfNotNull(CollectionCreated, this, new CollectionCreatedEventArgs + { + Collection = collection, + Options = options + + }, _logger); + + return collection; + } + finally + { + // Refresh handled internally + _iLibraryMonitor.ReportFileSystemChangeComplete(path, false); + } + } + + private Folder GetParentFolder(Guid? parentId) + { + if (parentId.HasValue) + { + if (parentId.Value == Guid.Empty) + { + throw new ArgumentNullException("parentId"); + } + + var folder = _libraryManager.GetItemById(parentId.Value) as Folder; + + // Find an actual physical folder + if (folder is CollectionFolder) + { + var child = _libraryManager.RootFolder.Children.OfType() + .FirstOrDefault(i => folder.PhysicalLocations.Contains(i.Path, StringComparer.OrdinalIgnoreCase)); + + if (child != null) + { + return child; + } + } + } + + return GetCollectionsFolder(string.Empty); + } + + public Task AddToCollection(Guid collectionId, IEnumerable ids) + { + return AddToCollection(collectionId, ids, true, new MetadataRefreshOptions(_fileSystem)); + } + + private async Task AddToCollection(Guid collectionId, IEnumerable ids, bool fireEvent, MetadataRefreshOptions refreshOptions) + { + var collection = _libraryManager.GetItemById(collectionId) as BoxSet; + + if (collection == null) + { + throw new ArgumentException("No collection exists with the supplied Id"); + } + + var list = new List(); + var itemList = new List(); + var currentLinkedChildren = collection.GetLinkedChildren().ToList(); + + foreach (var itemId in ids) + { + var item = _libraryManager.GetItemById(itemId); + + if (item == null) + { + throw new ArgumentException("No item exists with the supplied Id"); + } + + itemList.Add(item); + + if (currentLinkedChildren.All(i => i.Id != itemId)) + { + list.Add(LinkedChild.Create(item)); + } + } + + if (list.Count > 0) + { + collection.LinkedChildren.AddRange(list); + + collection.UpdateRatingToContent(); + + await collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); + + _providerManager.QueueRefresh(collection.Id, refreshOptions); + + if (fireEvent) + { + EventHelper.FireEventIfNotNull(ItemsAddedToCollection, this, new CollectionModifiedEventArgs + { + Collection = collection, + ItemsChanged = itemList + + }, _logger); + } + } + } + + public async Task RemoveFromCollection(Guid collectionId, IEnumerable itemIds) + { + var collection = _libraryManager.GetItemById(collectionId) as BoxSet; + + if (collection == null) + { + throw new ArgumentException("No collection exists with the supplied Id"); + } + + var list = new List(); + var itemList = new List(); + + foreach (var itemId in itemIds) + { + var childItem = _libraryManager.GetItemById(itemId); + + var child = collection.LinkedChildren.FirstOrDefault(i => (i.ItemId.HasValue && i.ItemId.Value == itemId) || (childItem != null && string.Equals(childItem.Path, i.Path, StringComparison.OrdinalIgnoreCase))); + + if (child == null) + { + throw new ArgumentException("No collection title exists with the supplied Id"); + } + + list.Add(child); + + if (childItem != null) + { + itemList.Add(childItem); + } + } + + foreach (var child in list) + { + collection.LinkedChildren.Remove(child); + } + + collection.UpdateRatingToContent(); + + await collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); + _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(_fileSystem)); + + EventHelper.FireEventIfNotNull(ItemsRemovedFromCollection, this, new CollectionModifiedEventArgs + { + Collection = collection, + ItemsChanged = itemList + + }, _logger); + } + + public IEnumerable CollapseItemsWithinBoxSets(IEnumerable items, User user) + { + var results = new Dictionary(); + + var allBoxsets = GetCollections(user).ToList(); + + foreach (var item in items) + { + var grouping = item as ISupportsBoxSetGrouping; + + if (grouping == null) + { + results[item.Id] = item; + } + else + { + var itemId = item.Id; + + var currentBoxSets = allBoxsets + .Where(i => i.GetLinkedChildren().Any(j => j.Id == itemId)) + .ToList(); + + if (currentBoxSets.Count > 0) + { + foreach (var boxset in currentBoxSets) + { + results[boxset.Id] = boxset; + } + } + else + { + results[item.Id] = item; + } + } + } + + return results.Values; + } + } +} -- cgit v1.2.3 From b76a1abda578b8ff64bad2997b036b0fc43e264f Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Thu, 3 Nov 2016 03:14:14 -0400 Subject: move classes to portable server lib --- .../Cryptography/CryptographyProvider.cs | 9 + .../Activity/ActivityLogEntryPoint.cs | 561 +++++++++++ .../Collections/CollectionImageProvider.cs | 84 ++ .../Devices/DeviceManager.cs | 306 ++++++ .../Emby.Server.Implementations.csproj | 13 + .../Images/BaseDynamicImageProvider.cs | 361 +++++++ .../Library/UserDataManager.cs | 287 ++++++ Emby.Server.Implementations/Library/UserManager.cs | 1029 ++++++++++++++++++++ .../Notifications/Notifications.cs | 547 +++++++++++ .../Notifications/WebSocketNotifier.cs | 54 + .../Photos/PhotoAlbumImageProvider.cs | 34 + .../Playlists/PlaylistImageProvider.cs | 104 ++ Emby.Server.Implementations/TV/TVSeriesManager.cs | 226 +++++ .../UserViews/CollectionFolderImageProvider.cs | 176 ++++ .../UserViews/DynamicImageProvider.cs | 188 ++++ .../Cryptography/ICryptographyProvider.cs | 1 + .../Collections/CollectionImageProvider.cs | 86 -- .../Devices/DeviceManager.cs | 306 ------ .../EntryPoints/ActivityLogEntryPoint.cs | 561 ----------- .../EntryPoints/Notifications/Notifications.cs | 544 ----------- .../EntryPoints/Notifications/WebSocketNotifier.cs | 54 - .../Library/UserDataManager.cs | 287 ------ .../Library/UserManager.cs | 1021 ------------------- .../MediaBrowser.Server.Implementations.csproj | 13 - .../Photos/BaseDynamicImageProvider.cs | 361 ------- .../Photos/PhotoAlbumImageProvider.cs | 35 - .../Playlists/PlaylistImageProvider.cs | 104 -- .../TV/TVSeriesManager.cs | 226 ----- .../UserViews/CollectionFolderImageProvider.cs | 176 ---- .../UserViews/DynamicImageProvider.cs | 190 ---- .../ApplicationHost.cs | 9 +- 31 files changed, 3983 insertions(+), 3970 deletions(-) create mode 100644 Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs create mode 100644 Emby.Server.Implementations/Collections/CollectionImageProvider.cs create mode 100644 Emby.Server.Implementations/Devices/DeviceManager.cs create mode 100644 Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs create mode 100644 Emby.Server.Implementations/Library/UserDataManager.cs create mode 100644 Emby.Server.Implementations/Library/UserManager.cs create mode 100644 Emby.Server.Implementations/Notifications/Notifications.cs create mode 100644 Emby.Server.Implementations/Notifications/WebSocketNotifier.cs create mode 100644 Emby.Server.Implementations/Photos/PhotoAlbumImageProvider.cs create mode 100644 Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs create mode 100644 Emby.Server.Implementations/TV/TVSeriesManager.cs create mode 100644 Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs create mode 100644 Emby.Server.Implementations/UserViews/DynamicImageProvider.cs delete mode 100644 MediaBrowser.Server.Implementations/Collections/CollectionImageProvider.cs delete mode 100644 MediaBrowser.Server.Implementations/Devices/DeviceManager.cs delete mode 100644 MediaBrowser.Server.Implementations/EntryPoints/ActivityLogEntryPoint.cs delete mode 100644 MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs delete mode 100644 MediaBrowser.Server.Implementations/EntryPoints/Notifications/WebSocketNotifier.cs delete mode 100644 MediaBrowser.Server.Implementations/Library/UserDataManager.cs delete mode 100644 MediaBrowser.Server.Implementations/Library/UserManager.cs delete mode 100644 MediaBrowser.Server.Implementations/Photos/BaseDynamicImageProvider.cs delete mode 100644 MediaBrowser.Server.Implementations/Photos/PhotoAlbumImageProvider.cs delete mode 100644 MediaBrowser.Server.Implementations/Playlists/PlaylistImageProvider.cs delete mode 100644 MediaBrowser.Server.Implementations/TV/TVSeriesManager.cs delete mode 100644 MediaBrowser.Server.Implementations/UserViews/CollectionFolderImageProvider.cs delete mode 100644 MediaBrowser.Server.Implementations/UserViews/DynamicImageProvider.cs (limited to 'Emby.Server.Implementations/Collections') diff --git a/Emby.Common.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Common.Implementations/Cryptography/CryptographyProvider.cs index 5ece28dc67..e5e4b1c7c1 100644 --- a/Emby.Common.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Common.Implementations/Cryptography/CryptographyProvider.cs @@ -19,6 +19,15 @@ namespace Emby.Common.Implementations.Cryptography return provider.ComputeHash(Encoding.Unicode.GetBytes(str)); } } + + public byte[] GetSHA1Bytes(byte[] bytes) + { + using (var provider = SHA1.Create()) + { + return provider.ComputeHash(bytes); + } + } + public byte[] GetMD5Bytes(Stream str) { using (var provider = MD5.Create()) diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs new file mode 100644 index 0000000000..11fd3a872e --- /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 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 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 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 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 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 e) + { + CreateLogEntry(new ActivityLogEntry + { + Name = string.Format(_localization.GetLocalizedString("UserDeletedWithName"), e.Argument.Name), + Type = "UserDeleted" + }); + } + + void _userManager_UserPasswordChanged(object sender, GenericEventArgs 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 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> 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 e) + { + CreateLogEntry(new ActivityLogEntry + { + Name = string.Format(_localization.GetLocalizedString("PluginUninstalledWithName"), e.Argument.Name), + Type = "PluginUninstalled" + }); + } + + void _installationManager_PluginInstalled(object sender, GenericEventArgs 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 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(); + + 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; + } + + /// + /// Constructs a user-friendly string for this TimeSpan instance. + /// + public static string ToUserFriendlyString(TimeSpan span) + { + const int DaysInYear = 365; + const int DaysInMonth = 30; + + // Get each non-zero value from TimeSpan component + List values = new List(); + + // 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(); + } + + /// + /// Constructs a string description of a time-span value. + /// + /// The value of this item + /// The name of this item (singular form) + 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 0000000000..b82d4e44e0 --- /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 + { + 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> 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 CreateImage(IHasImages item, List 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 0000000000..cdf636e22d --- /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> CameraImageUploaded; + + /// + /// Occurs when [device options updated]. + /// + public event EventHandler> 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 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 GetDevices(DeviceQuery query) + { + IEnumerable 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 + { + 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 + { + 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(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 GetConfigurations() + { + return new List + { + new ConfigurationStore + { + Key = "devices", + ConfigurationType = typeof(DevicesOptions) + } + }; + } + } + + public static class UploadConfigExtension + { + public static DevicesOptions GetUploadOptions(this IConfigurationManager config) + { + return config.GetConfiguration("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 11b3393c8a..567c9b99ed 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -52,6 +52,7 @@ Properties\SharedVersion.cs + @@ -60,7 +61,9 @@ + + @@ -69,6 +72,7 @@ + @@ -94,6 +98,8 @@ + + @@ -109,7 +115,11 @@ + + + + @@ -149,7 +159,10 @@ + + + diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs new file mode 100644 index 0000000000..224cd056aa --- /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 : IHasItemChangeMonitor, IForcedProvider, ICustomMetadataProvider, 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 GetSupportedImages(IHasImages item) + { + return new List + { + ImageType.Primary, + ImageType.Thumb + }; + } + + private IEnumerable 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 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 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 FetchToFileInternal(IHasImages item, + List 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> GetItemsWithImages(IHasImages item); + + protected Task CreateThumbCollage(IHasImages primaryItem, List items, string outputPath) + { + return CreateCollage(primaryItem, items, outputPath, 640, 360); + } + + protected virtual IEnumerable GetStripCollageImagePaths(IHasImages primaryItem, IEnumerable 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 CreatePosterCollage(IHasImages primaryItem, List items, string outputPath) + { + return CreateCollage(primaryItem, items, outputPath, 400, 600); + } + + protected Task CreateSquareCollage(IHasImages primaryItem, List items, string outputPath) + { + return CreateCollage(primaryItem, items, outputPath, 600, 600); + } + + protected Task CreateThumbCollage(IHasImages primaryItem, List items, string outputPath, int width, int height) + { + return CreateCollage(primaryItem, items, outputPath, width, height); + } + + private async Task CreateCollage(IHasImages primaryItem, List 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 CreateImage(IHasImages item, + List 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 GetFinalItems(List items) + { + return GetFinalItems(items, 4); + } + + protected virtual List GetFinalItems(List 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 CreateSingleImage(List 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 0000000000..c8dde1287a --- /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 +{ + /// + /// Class UserDataManager + /// + public class UserDataManager : IUserDataManager + { + public event EventHandler UserDataSaved; + + private readonly ConcurrentDictionary _userData = + new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + private readonly ILogger _logger; + private readonly IServerConfigurationManager _config; + + public UserDataManager(ILogManager logManager, IServerConfigurationManager config) + { + _config = config; + _logger = logManager.GetLogger(GetType().Name); + } + + /// + /// Gets or sets the repository. + /// + /// The repository. + 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); + } + + /// + /// Save the provided user data for the given user. Batch operation. Does not fire any events or update the cache. + /// + /// + /// + /// + /// + public async Task SaveAllUserData(Guid userId, IEnumerable 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); + } + + /// + /// Retrieve all user data for the given user + /// + /// + /// + public IEnumerable GetAllUserData(Guid userId) + { + if (userId == Guid.Empty) + { + throw new ArgumentNullException("userId"); + } + + return Repository.GetAllUserData(userId); + } + + public UserItemData GetUserData(Guid userId, Guid itemId, List 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 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; + } + + /// + /// Gets the internal key. + /// + /// System.String. + 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 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 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; + } + + /// + /// Converts a UserItemData to a DTOUserItemData + /// + /// The data. + /// DtoUserItemData. + /// + 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 0000000000..9c1d7fdf13 --- /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 +{ + /// + /// Class UserManager + /// + public class UserManager : IUserManager + { + /// + /// Gets the users. + /// + /// The users. + public IEnumerable Users { get; private set; } + + /// + /// The _logger + /// + private readonly ILogger _logger; + + /// + /// Gets or sets the configuration manager. + /// + /// The configuration manager. + private IServerConfigurationManager ConfigurationManager { get; set; } + + /// + /// Gets the active user repository + /// + /// The user repository. + private IUserRepository UserRepository { get; set; } + public event EventHandler> UserPasswordChanged; + + private readonly IXmlSerializer _xmlSerializer; + private readonly IJsonSerializer _jsonSerializer; + + private readonly INetworkManager _networkManager; + + private readonly Func _imageProcessorFactory; + private readonly Func _dtoServiceFactory; + private readonly Func _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 imageProcessorFactory, Func dtoServiceFactory, Func 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(); + + DeletePinFile(); + } + + #region UserUpdated Event + /// + /// Occurs when [user updated]. + /// + public event EventHandler> UserUpdated; + public event EventHandler> UserConfigurationUpdated; + public event EventHandler> UserLockedOut; + + /// + /// Called when [user updated]. + /// + /// The user. + private void OnUserUpdated(User user) + { + EventHelper.FireEventIfNotNull(UserUpdated, this, new GenericEventArgs { Argument = user }, _logger); + } + #endregion + + #region UserDeleted Event + /// + /// Occurs when [user deleted]. + /// + public event EventHandler> UserDeleted; + /// + /// Called when [user deleted]. + /// + /// The user. + private void OnUserDeleted(User user) + { + EventHelper.QueueEventIfNotNull(UserDeleted, this, new GenericEventArgs { Argument = user }, _logger); + } + #endregion + + /// + /// Gets a User by Id + /// + /// The id. + /// User. + /// + public User GetUserById(Guid id) + { + if (id == Guid.Empty) + { + throw new ArgumentNullException("id"); + } + + return Users.FirstOrDefault(u => u.Id == id); + } + + /// + /// Gets the user by identifier. + /// + /// The identifier. + /// User. + 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 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 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), _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); + } + + /// + /// Gets the sha1 string. + /// + /// The STR. + /// System.String. + private string GetSha1String(string str) + { + return BitConverter.ToString(_cryptographyProvider.GetSHA1Bytes(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty); + } + + /// + /// Loads the users from the repository + /// + /// IEnumerable{User}. + private async Task> 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; + } + } + + /// + /// Refreshes metadata for each user + /// + /// The cancellation token. + /// Task. + public Task RefreshUsersMetadata(CancellationToken cancellationToken) + { + var tasks = Users.Select(user => user.RefreshMetadata(new MetadataRefreshOptions(_fileSystem), cancellationToken)).ToList(); + + return Task.WhenAll(tasks); + } + + /// + /// Renames the user. + /// + /// The user. + /// The new name. + /// Task. + /// user + /// + 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); + } + + /// + /// Updates the user. + /// + /// The user. + /// user + /// + 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> UserCreated; + + private readonly SemaphoreSlim _userListLock = new SemaphoreSlim(1, 1); + + /// + /// Creates the user. + /// + /// The name. + /// User. + /// name + /// + public async Task 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 { Argument = user }, _logger); + + return user; + } + finally + { + _userListLock.Release(); + } + } + + /// + /// Deletes the user. + /// + /// The user. + /// Task. + /// user + /// + 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(); + } + } + + /// + /// Resets the password by clearing it. + /// + /// Task. + 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), _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), _logger); + } + + /// + /// Instantiates the new user. + /// + /// The name. + /// User. + 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 RedeemPasswordResetPin(string pin) + { + DeletePinFile(); + + var usersReset = new List(); + + 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(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(json); + } + + _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); + + lock (_configSyncLock) + { + _xmlSerializer.SerializeToFile(config, path); + user.Configuration = config; + } + + if (fireEvent) + { + EventHelper.FireEventIfNotNull(UserConfigurationUpdated, this, new GenericEventArgs { 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 0000000000..2d441c18cc --- /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 +{ + /// + /// Creates notifications for various system events + /// + 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 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 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 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> 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 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("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 _itemsAdded = new List(); + 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 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 e) + { + var notification = new NotificationRequest + { + UserIds = new List { 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 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 0000000000..8b3367217c --- /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 +{ + /// + /// Notifies clients anytime a notification is added or udpated + /// + 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 0000000000..cc1756f960 --- /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 + { + public PhotoAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor) + : base(fileSystem, providerManager, applicationPaths, imageProcessor) + { + } + + protected override Task> GetItemsWithImages(IHasImages item) + { + var photoAlbum = (PhotoAlbum)item; + var items = GetFinalItems(photoAlbum.Children.ToList()); + + return Task.FromResult(items); + } + + protected override Task CreateImage(IHasImages item, List 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 0000000000..ef7d6dba82 --- /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 + { + public PlaylistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor) : base(fileSystem, providerManager, applicationPaths, imageProcessor) + { + } + + protected override Task> 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 + { + 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> 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 CreateImage(IHasImages item, List 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 0000000000..f3bab78834 --- /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 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(); + + // Avoid implicitly captured closure + var episodes = GetNextUpEpisodes(request, user, items); + + return GetResult(episodes, null, request); + } + + public QueryResult GetNextUp(NextUpQuery request, IEnumerable 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(); + + // Avoid implicitly captured closure + var episodes = GetNextUpEpisodes(request, user, items); + + return GetResult(episodes, null, request); + } + + public IEnumerable GetNextUpEpisodes(NextUpQuery request, User user, IEnumerable 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(); + } + + /// + /// Gets the next up. + /// + /// The series. + /// The user. + /// Task{Episode}. + private Tuple 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().FirstOrDefault(); + + if (lastWatchedEpisode != null && firstUnwatchedEpisode != null) + { + var userData = _userDataManager.GetUserData(user, lastWatchedEpisode); + + var lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue.AddDays(1); + + return new Tuple(firstUnwatchedEpisode, lastWatchedDate, false); + } + + // Return the first episode + return new Tuple(firstUnwatchedEpisode, DateTime.MinValue, true); + } + + private QueryResult GetResult(IEnumerable 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 + { + 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 0000000000..ab63072386 --- /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 + { + public CollectionFolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor) : base(fileSystem, providerManager, applicationPaths, imageProcessor) + { + } + + public override IEnumerable GetSupportedImages(IHasImages item) + { + return new List + { + ImageType.Primary + }; + } + + protected override async Task> 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 CreateImage(IHasImages item, List 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 + { + 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 GetSupportedImages(IHasImages item) + { + return new List + { + ImageType.Primary + }; + } + + protected override async Task> 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 CreateImage(IHasImages item, List 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 0000000000..09b68c8ea3 --- /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 + { + 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 GetSupportedImages(IHasImages item) + { + var view = (UserView)item; + if (IsUsingCollectionStrip(view)) + { + return new List + { + ImageType.Primary + }; + } + + return new List + { + ImageType.Primary + }; + } + + protected override async Task> 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 CreateImage(IHasImages item, List 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); + } + } +} diff --git a/MediaBrowser.Model/Cryptography/ICryptographyProvider.cs b/MediaBrowser.Model/Cryptography/ICryptographyProvider.cs index 70f6798562..696a7c1009 100644 --- a/MediaBrowser.Model/Cryptography/ICryptographyProvider.cs +++ b/MediaBrowser.Model/Cryptography/ICryptographyProvider.cs @@ -7,6 +7,7 @@ namespace MediaBrowser.Model.Cryptography { Guid GetMD5(string str); byte[] GetMD5Bytes(string str); + byte[] GetSHA1Bytes(byte[] bytes); byte[] GetMD5Bytes(Stream str); } } \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Collections/CollectionImageProvider.cs b/MediaBrowser.Server.Implementations/Collections/CollectionImageProvider.cs deleted file mode 100644 index d70d4b9e65..0000000000 --- a/MediaBrowser.Server.Implementations/Collections/CollectionImageProvider.cs +++ /dev/null @@ -1,86 +0,0 @@ -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 MediaBrowser.Server.Implementations.Photos; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using MediaBrowser.Common.IO; -using MediaBrowser.Controller.IO; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Extensions; - -namespace MediaBrowser.Server.Implementations.Collections -{ - public class CollectionImageProvider : BaseDynamicImageProvider - { - 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> 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 CreateImage(IHasImages item, List itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) - { - return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary); - } - } -} diff --git a/MediaBrowser.Server.Implementations/Devices/DeviceManager.cs b/MediaBrowser.Server.Implementations/Devices/DeviceManager.cs deleted file mode 100644 index e2e59df088..0000000000 --- a/MediaBrowser.Server.Implementations/Devices/DeviceManager.cs +++ /dev/null @@ -1,306 +0,0 @@ -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 MediaBrowser.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> CameraImageUploaded; - - /// - /// Occurs when [device options updated]. - /// - public event EventHandler> 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 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 GetDevices(DeviceQuery query) - { - IEnumerable 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 - { - 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 - { - 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(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 GetConfigurations() - { - return new List - { - new ConfigurationStore - { - Key = "devices", - ConfigurationType = typeof(DevicesOptions) - } - }; - } - } - - public static class UploadConfigExtension - { - public static DevicesOptions GetUploadOptions(this IConfigurationManager config) - { - return config.GetConfiguration("devices"); - } - } -} \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/EntryPoints/ActivityLogEntryPoint.cs b/MediaBrowser.Server.Implementations/EntryPoints/ActivityLogEntryPoint.cs deleted file mode 100644 index 51f5f57b3e..0000000000 --- a/MediaBrowser.Server.Implementations/EntryPoints/ActivityLogEntryPoint.cs +++ /dev/null @@ -1,561 +0,0 @@ -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 MediaBrowser.Server.Implementations.EntryPoints -{ - 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 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 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 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 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 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 e) - { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format(_localization.GetLocalizedString("UserDeletedWithName"), e.Argument.Name), - Type = "UserDeleted" - }); - } - - void _userManager_UserPasswordChanged(object sender, GenericEventArgs 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 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> 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 e) - { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format(_localization.GetLocalizedString("PluginUninstalledWithName"), e.Argument.Name), - Type = "PluginUninstalled" - }); - } - - void _installationManager_PluginInstalled(object sender, GenericEventArgs 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 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(); - - 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; - } - - /// - /// Constructs a user-friendly string for this TimeSpan instance. - /// - public static string ToUserFriendlyString(TimeSpan span) - { - const int DaysInYear = 365; - const int DaysInMonth = 30; - - // Get each non-zero value from TimeSpan component - List values = new List(); - - // 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(); - } - - /// - /// Constructs a string description of a time-span value. - /// - /// The value of this item - /// The name of this item (singular form) - 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/MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs b/MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs deleted file mode 100644 index f3d1dc8f9b..0000000000 --- a/MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs +++ /dev/null @@ -1,544 +0,0 @@ -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; - -namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications -{ - /// - /// Creates notifications for various system events - /// - 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 Timer 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) - { - _installationManager = installationManager; - _userManager = userManager; - _logger = logger; - _taskManager = taskManager; - _notificationManager = notificationManager; - _libraryManager = libraryManager; - _sessionManager = sessionManager; - _appHost = appHost; - _config = config; - _deviceManager = deviceManager; - } - - 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 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 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 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> 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 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("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 _itemsAdded = new List(); - void _libraryManager_ItemAdded(object sender, ItemChangeEventArgs e) - { - if (!FilterItem(e.Item)) - { - return; - } - - lock (_libraryChangedSyncLock) - { - if (LibraryUpdateTimer == null) - { - LibraryUpdateTimer = new Timer(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 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 e) - { - var notification = new NotificationRequest - { - UserIds = new List { 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 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/MediaBrowser.Server.Implementations/EntryPoints/Notifications/WebSocketNotifier.cs b/MediaBrowser.Server.Implementations/EntryPoints/Notifications/WebSocketNotifier.cs deleted file mode 100644 index 916b4a6224..0000000000 --- a/MediaBrowser.Server.Implementations/EntryPoints/Notifications/WebSocketNotifier.cs +++ /dev/null @@ -1,54 +0,0 @@ -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Notifications; -using MediaBrowser.Controller.Plugins; -using System.Linq; - -namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications -{ - /// - /// Notifies clients anytime a notification is added or udpated - /// - 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/MediaBrowser.Server.Implementations/Library/UserDataManager.cs b/MediaBrowser.Server.Implementations/Library/UserDataManager.cs deleted file mode 100644 index 9ee65a57c6..0000000000 --- a/MediaBrowser.Server.Implementations/Library/UserDataManager.cs +++ /dev/null @@ -1,287 +0,0 @@ -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 MediaBrowser.Server.Implementations.Library -{ - /// - /// Class UserDataManager - /// - public class UserDataManager : IUserDataManager - { - public event EventHandler UserDataSaved; - - private readonly ConcurrentDictionary _userData = - new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - - private readonly ILogger _logger; - private readonly IServerConfigurationManager _config; - - public UserDataManager(ILogManager logManager, IServerConfigurationManager config) - { - _config = config; - _logger = logManager.GetLogger(GetType().Name); - } - - /// - /// Gets or sets the repository. - /// - /// The repository. - 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); - } - - /// - /// Save the provided user data for the given user. Batch operation. Does not fire any events or update the cache. - /// - /// - /// - /// - /// - public async Task SaveAllUserData(Guid userId, IEnumerable 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); - } - - /// - /// Retrieve all user data for the given user - /// - /// - /// - public IEnumerable GetAllUserData(Guid userId) - { - if (userId == Guid.Empty) - { - throw new ArgumentNullException("userId"); - } - - return Repository.GetAllUserData(userId); - } - - public UserItemData GetUserData(Guid userId, Guid itemId, List 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 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; - } - - /// - /// Gets the internal key. - /// - /// System.String. - 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 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 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; - } - - /// - /// Converts a UserItemData to a DTOUserItemData - /// - /// The data. - /// DtoUserItemData. - /// - 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/MediaBrowser.Server.Implementations/Library/UserManager.cs b/MediaBrowser.Server.Implementations/Library/UserManager.cs deleted file mode 100644 index 794c924eb6..0000000000 --- a/MediaBrowser.Server.Implementations/Library/UserManager.cs +++ /dev/null @@ -1,1021 +0,0 @@ -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.Security.Cryptography; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.IO; -using MediaBrowser.Controller.IO; -using MediaBrowser.Model.IO; - -namespace MediaBrowser.Server.Implementations.Library -{ - /// - /// Class UserManager - /// - public class UserManager : IUserManager - { - /// - /// Gets the users. - /// - /// The users. - public IEnumerable Users { get; private set; } - - /// - /// The _logger - /// - private readonly ILogger _logger; - - /// - /// Gets or sets the configuration manager. - /// - /// The configuration manager. - private IServerConfigurationManager ConfigurationManager { get; set; } - - /// - /// Gets the active user repository - /// - /// The user repository. - private IUserRepository UserRepository { get; set; } - public event EventHandler> UserPasswordChanged; - - private readonly IXmlSerializer _xmlSerializer; - private readonly IJsonSerializer _jsonSerializer; - - private readonly INetworkManager _networkManager; - - private readonly Func _imageProcessorFactory; - private readonly Func _dtoServiceFactory; - private readonly Func _connectFactory; - private readonly IServerApplicationHost _appHost; - private readonly IFileSystem _fileSystem; - - public UserManager(ILogger logger, IServerConfigurationManager configurationManager, IUserRepository userRepository, IXmlSerializer xmlSerializer, INetworkManager networkManager, Func imageProcessorFactory, Func dtoServiceFactory, Func connectFactory, IServerApplicationHost appHost, IJsonSerializer jsonSerializer, IFileSystem fileSystem) - { - _logger = logger; - UserRepository = userRepository; - _xmlSerializer = xmlSerializer; - _networkManager = networkManager; - _imageProcessorFactory = imageProcessorFactory; - _dtoServiceFactory = dtoServiceFactory; - _connectFactory = connectFactory; - _appHost = appHost; - _jsonSerializer = jsonSerializer; - _fileSystem = fileSystem; - ConfigurationManager = configurationManager; - Users = new List(); - - DeletePinFile(); - } - - #region UserUpdated Event - /// - /// Occurs when [user updated]. - /// - public event EventHandler> UserUpdated; - public event EventHandler> UserConfigurationUpdated; - public event EventHandler> UserLockedOut; - - /// - /// Called when [user updated]. - /// - /// The user. - private void OnUserUpdated(User user) - { - EventHelper.FireEventIfNotNull(UserUpdated, this, new GenericEventArgs { Argument = user }, _logger); - } - #endregion - - #region UserDeleted Event - /// - /// Occurs when [user deleted]. - /// - public event EventHandler> UserDeleted; - /// - /// Called when [user deleted]. - /// - /// The user. - private void OnUserDeleted(User user) - { - EventHelper.QueueEventIfNotNull(UserDeleted, this, new GenericEventArgs { Argument = user }, _logger); - } - #endregion - - /// - /// Gets a User by Id - /// - /// The id. - /// User. - /// - public User GetUserById(Guid id) - { - if (id == Guid.Empty) - { - throw new ArgumentNullException("id"); - } - - return Users.FirstOrDefault(u => u.Id == id); - } - - /// - /// Gets the user by identifier. - /// - /// The identifier. - /// User. - 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 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 (.) - return username.All(IsValidUsernameCharacter); - } - - 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 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), _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); - } - - /// - /// Gets the sha1 string. - /// - /// The STR. - /// System.String. - private static string GetSha1String(string str) - { - using (var provider = SHA1.Create()) - { - var hash = provider.ComputeHash(Encoding.UTF8.GetBytes(str)); - return BitConverter.ToString(hash).Replace("-", string.Empty); - } - } - - /// - /// Loads the users from the repository - /// - /// IEnumerable{User}. - private async Task> LoadUsers() - { - var users = UserRepository.RetrieveAllUsers().ToList(); - - // There always has to be at least one user. - if (users.Count == 0) - { - var name = MakeValidUsername(Environment.UserName); - - 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; - } - } - - /// - /// Refreshes metadata for each user - /// - /// The cancellation token. - /// Task. - public Task RefreshUsersMetadata(CancellationToken cancellationToken) - { - var tasks = Users.Select(user => user.RefreshMetadata(new MetadataRefreshOptions(_fileSystem), cancellationToken)).ToList(); - - return Task.WhenAll(tasks); - } - - /// - /// Renames the user. - /// - /// The user. - /// The new name. - /// Task. - /// user - /// - 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); - } - - /// - /// Updates the user. - /// - /// The user. - /// user - /// - 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> UserCreated; - - private readonly SemaphoreSlim _userListLock = new SemaphoreSlim(1, 1); - - /// - /// Creates the user. - /// - /// The name. - /// User. - /// name - /// - public async Task 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 { Argument = user }, _logger); - - return user; - } - finally - { - _userListLock.Release(); - } - } - - /// - /// Deletes the user. - /// - /// The user. - /// Task. - /// user - /// - 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(); - } - } - - /// - /// Resets the password by clearing it. - /// - /// Task. - 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), _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), _logger); - } - - /// - /// Instantiates the new user. - /// - /// The name. - /// User. - 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); - text.AppendLine("The pin code will expire at " + expiration.ToLocalTime().ToShortDateString() + " " + expiration.ToLocalTime().ToShortTimeString()); - - _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 RedeemPasswordResetPin(string pin) - { - DeletePinFile(); - - var usersReset = new List(); - - 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 (DirectoryNotFoundException) - { - return GetDefaultPolicy(user); - } - catch (FileNotFoundException) - { - 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(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 (DirectoryNotFoundException) - { - return new UserConfiguration(); - } - catch (FileNotFoundException) - { - 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(json); - } - - _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); - - lock (_configSyncLock) - { - _xmlSerializer.SerializeToFile(config, path); - user.Configuration = config; - } - - if (fireEvent) - { - EventHelper.FireEventIfNotNull(UserConfigurationUpdated, this, new GenericEventArgs { Argument = user }, _logger); - } - } - } -} \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index fc71ef8a08..3fb3ca8833 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -111,23 +111,18 @@ - - - - - @@ -162,8 +157,6 @@ - - @@ -237,8 +230,6 @@ - - @@ -246,10 +237,7 @@ - - - @@ -280,7 +268,6 @@ - diff --git a/MediaBrowser.Server.Implementations/Photos/BaseDynamicImageProvider.cs b/MediaBrowser.Server.Implementations/Photos/BaseDynamicImageProvider.cs deleted file mode 100644 index 0dd7672bc1..0000000000 --- a/MediaBrowser.Server.Implementations/Photos/BaseDynamicImageProvider.cs +++ /dev/null @@ -1,361 +0,0 @@ -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 MediaBrowser.Server.Implementations.Photos -{ - public abstract class BaseDynamicImageProvider : IHasItemChangeMonitor, IForcedProvider, ICustomMetadataProvider, 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 GetSupportedImages(IHasImages item) - { - return new List - { - ImageType.Primary, - ImageType.Thumb - }; - } - - private IEnumerable 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 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 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 FetchToFileInternal(IHasImages item, - List 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> GetItemsWithImages(IHasImages item); - - protected Task CreateThumbCollage(IHasImages primaryItem, List items, string outputPath) - { - return CreateCollage(primaryItem, items, outputPath, 640, 360); - } - - protected virtual IEnumerable GetStripCollageImagePaths(IHasImages primaryItem, IEnumerable 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 CreatePosterCollage(IHasImages primaryItem, List items, string outputPath) - { - return CreateCollage(primaryItem, items, outputPath, 400, 600); - } - - protected Task CreateSquareCollage(IHasImages primaryItem, List items, string outputPath) - { - return CreateCollage(primaryItem, items, outputPath, 600, 600); - } - - protected Task CreateThumbCollage(IHasImages primaryItem, List items, string outputPath, int width, int height) - { - return CreateCollage(primaryItem, items, outputPath, width, height); - } - - private async Task CreateCollage(IHasImages primaryItem, List 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 CreateImage(IHasImages item, - List 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 GetFinalItems(List items) - { - return GetFinalItems(items, 4); - } - - protected virtual List GetFinalItems(List 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 CreateSingleImage(List 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); - File.Copy(image, outputPath); - - return outputPath; - } - } -} diff --git a/MediaBrowser.Server.Implementations/Photos/PhotoAlbumImageProvider.cs b/MediaBrowser.Server.Implementations/Photos/PhotoAlbumImageProvider.cs deleted file mode 100644 index ab03f5aaa9..0000000000 --- a/MediaBrowser.Server.Implementations/Photos/PhotoAlbumImageProvider.cs +++ /dev/null @@ -1,35 +0,0 @@ -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 MediaBrowser.Common.IO; -using MediaBrowser.Controller.IO; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Server.Implementations.Photos -{ - public class PhotoAlbumImageProvider : BaseDynamicImageProvider - { - public PhotoAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor) - : base(fileSystem, providerManager, applicationPaths, imageProcessor) - { - } - - protected override Task> GetItemsWithImages(IHasImages item) - { - var photoAlbum = (PhotoAlbum)item; - var items = GetFinalItems(photoAlbum.Children.ToList()); - - return Task.FromResult(items); - } - - protected override Task CreateImage(IHasImages item, List itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) - { - return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary); - } - } -} diff --git a/MediaBrowser.Server.Implementations/Playlists/PlaylistImageProvider.cs b/MediaBrowser.Server.Implementations/Playlists/PlaylistImageProvider.cs deleted file mode 100644 index 0249b85ee5..0000000000 --- a/MediaBrowser.Server.Implementations/Playlists/PlaylistImageProvider.cs +++ /dev/null @@ -1,104 +0,0 @@ -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 MediaBrowser.Server.Implementations.Photos; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -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 MediaBrowser.Server.Implementations.Playlists -{ - public class PlaylistImageProvider : BaseDynamicImageProvider - { - public PlaylistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor) : base(fileSystem, providerManager, applicationPaths, imageProcessor) - { - } - - protected override Task> 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 - { - 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> 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 CreateImage(IHasImages item, List itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) - //{ - // return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary); - //} - } - -} diff --git a/MediaBrowser.Server.Implementations/TV/TVSeriesManager.cs b/MediaBrowser.Server.Implementations/TV/TVSeriesManager.cs deleted file mode 100644 index 03e8a9178e..0000000000 --- a/MediaBrowser.Server.Implementations/TV/TVSeriesManager.cs +++ /dev/null @@ -1,226 +0,0 @@ -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 MediaBrowser.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 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(); - - // Avoid implicitly captured closure - var episodes = GetNextUpEpisodes(request, user, items); - - return GetResult(episodes, null, request); - } - - public QueryResult GetNextUp(NextUpQuery request, IEnumerable 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(); - - // Avoid implicitly captured closure - var episodes = GetNextUpEpisodes(request, user, items); - - return GetResult(episodes, null, request); - } - - public IEnumerable GetNextUpEpisodes(NextUpQuery request, User user, IEnumerable 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(); - } - - /// - /// Gets the next up. - /// - /// The series. - /// The user. - /// Task{Episode}. - private Tuple 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().FirstOrDefault(); - - if (lastWatchedEpisode != null && firstUnwatchedEpisode != null) - { - var userData = _userDataManager.GetUserData(user, lastWatchedEpisode); - - var lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue.AddDays(1); - - return new Tuple(firstUnwatchedEpisode, lastWatchedDate, false); - } - - // Return the first episode - return new Tuple(firstUnwatchedEpisode, DateTime.MinValue, true); - } - - private QueryResult GetResult(IEnumerable 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 - { - TotalRecordCount = totalCount, - Items = itemsArray - }; - } - } -} diff --git a/MediaBrowser.Server.Implementations/UserViews/CollectionFolderImageProvider.cs b/MediaBrowser.Server.Implementations/UserViews/CollectionFolderImageProvider.cs deleted file mode 100644 index 33a7b67258..0000000000 --- a/MediaBrowser.Server.Implementations/UserViews/CollectionFolderImageProvider.cs +++ /dev/null @@ -1,176 +0,0 @@ -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 MediaBrowser.Server.Implementations.Photos; -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.Collections; -using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.IO; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Extensions; -using MediaBrowser.Model.Querying; - -namespace MediaBrowser.Server.Implementations.UserViews -{ - public class CollectionFolderImageProvider : BaseDynamicImageProvider - { - public CollectionFolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor) : base(fileSystem, providerManager, applicationPaths, imageProcessor) - { - } - - public override IEnumerable GetSupportedImages(IHasImages item) - { - return new List - { - ImageType.Primary - }; - } - - protected override async Task> 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 CreateImage(IHasImages item, List 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 - { - 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 GetSupportedImages(IHasImages item) - { - return new List - { - ImageType.Primary - }; - } - - protected override async Task> 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 CreateImage(IHasImages item, List 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/MediaBrowser.Server.Implementations/UserViews/DynamicImageProvider.cs b/MediaBrowser.Server.Implementations/UserViews/DynamicImageProvider.cs deleted file mode 100644 index 61f3c77f05..0000000000 --- a/MediaBrowser.Server.Implementations/UserViews/DynamicImageProvider.cs +++ /dev/null @@ -1,190 +0,0 @@ -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 MediaBrowser.Server.Implementations.Photos; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using MediaBrowser.Common.IO; -using MediaBrowser.Controller.IO; -using MediaBrowser.Model.IO; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.Extensions; - -namespace MediaBrowser.Server.Implementations.UserViews -{ - public class DynamicImageProvider : BaseDynamicImageProvider - { - 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 GetSupportedImages(IHasImages item) - { - var view = (UserView)item; - if (IsUsingCollectionStrip(view)) - { - return new List - { - ImageType.Primary - }; - } - - return new List - { - ImageType.Primary - }; - } - - protected override async Task> 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 CreateImage(IHasImages item, List 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); - } - } -} diff --git a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs index eb5a8a2e59..7c8945ff2e 100644 --- a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs +++ b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs @@ -50,7 +50,6 @@ using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Subtitles; using MediaBrowser.Server.Implementations; using MediaBrowser.Server.Implementations.Activity; -using MediaBrowser.Server.Implementations.Collections; using MediaBrowser.Server.Implementations.Configuration; using MediaBrowser.Server.Implementations.Connect; using MediaBrowser.Server.Implementations.Devices; @@ -58,19 +57,16 @@ using MediaBrowser.Server.Implementations.EntryPoints; using MediaBrowser.Server.Implementations.HttpServer; using MediaBrowser.Server.Implementations.HttpServer.Security; using MediaBrowser.Server.Implementations.IO; -using MediaBrowser.Server.Implementations.Library; using MediaBrowser.Server.Implementations.LiveTv; using MediaBrowser.Server.Implementations.Localization; using MediaBrowser.Server.Implementations.MediaEncoder; using MediaBrowser.Server.Implementations.Notifications; using MediaBrowser.Server.Implementations.Persistence; -using MediaBrowser.Server.Implementations.Playlists; using MediaBrowser.Server.Implementations.Security; using MediaBrowser.Server.Implementations.ServerManager; using MediaBrowser.Server.Implementations.Session; using MediaBrowser.Server.Implementations.Social; using MediaBrowser.Server.Implementations.Sync; -using MediaBrowser.Server.Implementations.TV; using MediaBrowser.Server.Startup.Common.FFMpeg; using MediaBrowser.Server.Startup.Common.Migrations; using MediaBrowser.WebDashboard.Api; @@ -98,7 +94,6 @@ using MediaBrowser.Common.Updates; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.IO; using Emby.Dlna; using Emby.Dlna.ConnectionManager; using Emby.Dlna.ContentDirectory; @@ -108,11 +103,13 @@ using Emby.Dlna.Ssdp; using Emby.Server.Implementations.Activity; using Emby.Server.Implementations.Channels; using Emby.Server.Implementations.Collections; +using Emby.Server.Implementations.Devices; using Emby.Server.Implementations.Dto; using Emby.Server.Implementations.FileOrganization; using Emby.Server.Implementations.Library; using Emby.Server.Implementations.Persistence; using Emby.Server.Implementations.Playlists; +using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Dlna; @@ -582,7 +579,7 @@ namespace MediaBrowser.Server.Startup.Common SyncRepository = await GetSyncRepository().ConfigureAwait(false); RegisterSingleInstance(SyncRepository); - UserManager = new UserManager(LogManager.GetLogger("UserManager"), ServerConfigurationManager, UserRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, () => ConnectManager, this, JsonSerializer, FileSystemManager); + UserManager = new UserManager(LogManager.GetLogger("UserManager"), ServerConfigurationManager, UserRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, () => ConnectManager, this, JsonSerializer, FileSystemManager, CryptographyProvider, Environment.UserName); RegisterSingleInstance(UserManager); LibraryManager = new LibraryManager(Logger, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager, () => UserViewManager); -- cgit v1.2.3 From 06afe47ee9716cb210067f3c09cd0c97722bd1c7 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Fri, 11 Nov 2016 01:43:42 -0500 Subject: update server core project --- Emby.Server.Core/Activity/ActivityRepository.cs | 252 + Emby.Server.Core/Data/BaseSqliteRepository.cs | 114 + Emby.Server.Core/Data/DataExtensions.cs | 181 + Emby.Server.Core/Data/IDbConnector.cs | 10 + Emby.Server.Core/Data/MediaStreamColumns.cs | 408 ++ .../Data/SqliteDisplayPreferencesRepository.cs | 312 ++ .../Data/SqliteFileOrganizationRepository.cs | 408 ++ Emby.Server.Core/Data/SqliteItemRepository.cs | 5423 ++++++++++++++++++++ Emby.Server.Core/Data/SqliteUserDataRepository.cs | 453 ++ Emby.Server.Core/Data/SqliteUserRepository.cs | 237 + Emby.Server.Core/Data/TypeMapper.cs | 47 + Emby.Server.Core/Devices/DeviceRepository.cs | 208 + Emby.Server.Core/Emby.Server.Core.xproj | 1 + .../EntryPoints/ExternalPortForwarding.cs | 292 ++ Emby.Server.Core/FFMpeg/FFMpegInfo.cs | 24 + Emby.Server.Core/FFMpeg/FFMpegInstallInfo.cs | 20 + Emby.Server.Core/Migrations/DbMigration.cs | 63 + Emby.Server.Core/Migrations/IVersionMigration.cs | 9 + .../Migrations/UpdateLevelMigration.cs | 130 + .../Notifications/SqliteNotificationsRepository.cs | 470 ++ Emby.Server.Core/Social/SharingRepository.cs | 158 + Emby.Server.Core/Sync/SyncRepository.cs | 976 ++++ Emby.Server.Core/project.json | 13 + .../Collections/CollectionsDynamicFolder.cs | 34 + .../Devices/CameraUploadsDynamicFolder.cs | 41 + .../Emby.Server.Implementations.csproj | 111 +- .../Playlists/PlaylistsDynamicFolder.cs | 32 + .../Devices/CameraUploadsFolder.cs | 67 + .../MediaBrowser.Controller.csproj | 2 + .../Playlists/ManualPlaylistsFolder.cs | 53 + .../Collections/CollectionsDynamicFolder.cs | 34 - .../Devices/CameraUploadsFolder.cs | 92 - .../MediaBrowser.Server.Implementations.csproj | 3 - .../Playlists/ManualPlaylistsFolder.cs | 77 - MediaBrowser.Server.Mono/Native/DbConnector.cs | 2 +- MediaBrowser.Server.Mono/Native/MonoApp.cs | 3 +- .../Activity/ActivityRepository.cs | 252 - .../ApplicationHost.cs | 16 +- .../Devices/DeviceRepository.cs | 208 - .../EntryPoints/ExternalPortForwarding.cs | 292 -- .../FFMpeg/FFMpegInfo.cs | 24 - .../FFMpeg/FFMpegInstallInfo.cs | 20 - .../FFMpeg/FFMpegLoader.cs | 1 + MediaBrowser.Server.Startup.Common/INativeApp.cs | 4 +- .../MediaBrowser.Server.Startup.Common.csproj | 60 +- .../Migrations/DbMigration.cs | 63 - .../Migrations/IVersionMigration.cs | 9 - .../Migrations/UpdateLevelMigration.cs | 130 - .../Notifications/SqliteNotificationsRepository.cs | 470 -- .../Persistence/BaseSqliteRepository.cs | 114 - .../Persistence/DataExtensions.cs | 181 - .../Persistence/IDbConnector.cs | 10 - .../Persistence/MediaStreamColumns.cs | 408 -- .../SqliteDisplayPreferencesRepository.cs | 312 -- .../Persistence/SqliteExtensions.cs | 2 +- .../SqliteFileOrganizationRepository.cs | 408 -- .../Persistence/SqliteItemRepository.cs | 5423 -------------------- .../Persistence/SqliteUserDataRepository.cs | 453 -- .../Persistence/SqliteUserRepository.cs | 237 - .../Persistence/TypeMapper.cs | 47 - .../Security/AuthenticationRepository.cs | 2 +- .../Social/SharingRepository.cs | 158 - .../Sync/SyncRepository.cs | 976 ---- MediaBrowser.ServerApplication/MainStartup.cs | 2 +- .../Native/DbConnector.cs | 2 +- .../Native/WindowsApp.cs | 3 +- MediaBrowser.ServerApplication/ServerNotifyIcon.cs | 2 +- 67 files changed, 10483 insertions(+), 10566 deletions(-) create mode 100644 Emby.Server.Core/Activity/ActivityRepository.cs create mode 100644 Emby.Server.Core/Data/BaseSqliteRepository.cs create mode 100644 Emby.Server.Core/Data/DataExtensions.cs create mode 100644 Emby.Server.Core/Data/IDbConnector.cs create mode 100644 Emby.Server.Core/Data/MediaStreamColumns.cs create mode 100644 Emby.Server.Core/Data/SqliteDisplayPreferencesRepository.cs create mode 100644 Emby.Server.Core/Data/SqliteFileOrganizationRepository.cs create mode 100644 Emby.Server.Core/Data/SqliteItemRepository.cs create mode 100644 Emby.Server.Core/Data/SqliteUserDataRepository.cs create mode 100644 Emby.Server.Core/Data/SqliteUserRepository.cs create mode 100644 Emby.Server.Core/Data/TypeMapper.cs create mode 100644 Emby.Server.Core/Devices/DeviceRepository.cs create mode 100644 Emby.Server.Core/EntryPoints/ExternalPortForwarding.cs create mode 100644 Emby.Server.Core/FFMpeg/FFMpegInfo.cs create mode 100644 Emby.Server.Core/FFMpeg/FFMpegInstallInfo.cs create mode 100644 Emby.Server.Core/Migrations/DbMigration.cs create mode 100644 Emby.Server.Core/Migrations/IVersionMigration.cs create mode 100644 Emby.Server.Core/Migrations/UpdateLevelMigration.cs create mode 100644 Emby.Server.Core/Notifications/SqliteNotificationsRepository.cs create mode 100644 Emby.Server.Core/Social/SharingRepository.cs create mode 100644 Emby.Server.Core/Sync/SyncRepository.cs create mode 100644 Emby.Server.Implementations/Collections/CollectionsDynamicFolder.cs create mode 100644 Emby.Server.Implementations/Devices/CameraUploadsDynamicFolder.cs create mode 100644 Emby.Server.Implementations/Playlists/PlaylistsDynamicFolder.cs create mode 100644 MediaBrowser.Controller/Devices/CameraUploadsFolder.cs create mode 100644 MediaBrowser.Controller/Playlists/ManualPlaylistsFolder.cs delete mode 100644 MediaBrowser.Server.Implementations/Collections/CollectionsDynamicFolder.cs delete mode 100644 MediaBrowser.Server.Implementations/Devices/CameraUploadsFolder.cs delete mode 100644 MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs delete mode 100644 MediaBrowser.Server.Startup.Common/Activity/ActivityRepository.cs delete mode 100644 MediaBrowser.Server.Startup.Common/Devices/DeviceRepository.cs delete mode 100644 MediaBrowser.Server.Startup.Common/EntryPoints/ExternalPortForwarding.cs delete mode 100644 MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegInfo.cs delete mode 100644 MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegInstallInfo.cs delete mode 100644 MediaBrowser.Server.Startup.Common/Migrations/DbMigration.cs delete mode 100644 MediaBrowser.Server.Startup.Common/Migrations/IVersionMigration.cs delete mode 100644 MediaBrowser.Server.Startup.Common/Migrations/UpdateLevelMigration.cs delete mode 100644 MediaBrowser.Server.Startup.Common/Notifications/SqliteNotificationsRepository.cs delete mode 100644 MediaBrowser.Server.Startup.Common/Persistence/BaseSqliteRepository.cs delete mode 100644 MediaBrowser.Server.Startup.Common/Persistence/DataExtensions.cs delete mode 100644 MediaBrowser.Server.Startup.Common/Persistence/IDbConnector.cs delete mode 100644 MediaBrowser.Server.Startup.Common/Persistence/MediaStreamColumns.cs delete mode 100644 MediaBrowser.Server.Startup.Common/Persistence/SqliteDisplayPreferencesRepository.cs delete mode 100644 MediaBrowser.Server.Startup.Common/Persistence/SqliteFileOrganizationRepository.cs delete mode 100644 MediaBrowser.Server.Startup.Common/Persistence/SqliteItemRepository.cs delete mode 100644 MediaBrowser.Server.Startup.Common/Persistence/SqliteUserDataRepository.cs delete mode 100644 MediaBrowser.Server.Startup.Common/Persistence/SqliteUserRepository.cs delete mode 100644 MediaBrowser.Server.Startup.Common/Persistence/TypeMapper.cs delete mode 100644 MediaBrowser.Server.Startup.Common/Social/SharingRepository.cs delete mode 100644 MediaBrowser.Server.Startup.Common/Sync/SyncRepository.cs (limited to 'Emby.Server.Implementations/Collections') diff --git a/Emby.Server.Core/Activity/ActivityRepository.cs b/Emby.Server.Core/Activity/ActivityRepository.cs new file mode 100644 index 0000000000..c11dcf7238 --- /dev/null +++ b/Emby.Server.Core/Activity/ActivityRepository.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.IO; +using System.Threading.Tasks; +using Emby.Server.Core.Data; +using MediaBrowser.Controller; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Querying; + +namespace Emby.Server.Core.Activity +{ + public class ActivityRepository : BaseSqliteRepository, IActivityRepository + { + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + public ActivityRepository(ILogManager logManager, IServerApplicationPaths appPaths, IDbConnector connector) + : base(logManager, connector) + { + DbFilePath = Path.Combine(appPaths.DataPath, "activitylog.db"); + } + + public async Task Initialize() + { + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + string[] queries = { + + "create table if not exists ActivityLogEntries (Id GUID PRIMARY KEY, Name TEXT, Overview TEXT, ShortOverview TEXT, Type TEXT, ItemId TEXT, UserId TEXT, DateCreated DATETIME, LogSeverity TEXT)", + "create index if not exists idx_ActivityLogEntries on ActivityLogEntries(Id)" + }; + + connection.RunQueries(queries, Logger); + } + } + + private const string BaseActivitySelectText = "select Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity from ActivityLogEntries"; + + public Task Create(ActivityLogEntry entry) + { + return Update(entry); + } + + public async Task Update(ActivityLogEntry entry) + { + if (entry == null) + { + throw new ArgumentNullException("entry"); + } + + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + using (var saveActivityCommand = connection.CreateCommand()) + { + saveActivityCommand.CommandText = "replace into ActivityLogEntries (Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) values (@Id, @Name, @Overview, @ShortOverview, @Type, @ItemId, @UserId, @DateCreated, @LogSeverity)"; + + saveActivityCommand.Parameters.Add(saveActivityCommand, "@Id"); + saveActivityCommand.Parameters.Add(saveActivityCommand, "@Name"); + saveActivityCommand.Parameters.Add(saveActivityCommand, "@Overview"); + saveActivityCommand.Parameters.Add(saveActivityCommand, "@ShortOverview"); + saveActivityCommand.Parameters.Add(saveActivityCommand, "@Type"); + saveActivityCommand.Parameters.Add(saveActivityCommand, "@ItemId"); + saveActivityCommand.Parameters.Add(saveActivityCommand, "@UserId"); + saveActivityCommand.Parameters.Add(saveActivityCommand, "@DateCreated"); + saveActivityCommand.Parameters.Add(saveActivityCommand, "@LogSeverity"); + + IDbTransaction transaction = null; + + try + { + transaction = connection.BeginTransaction(); + + var index = 0; + + saveActivityCommand.GetParameter(index++).Value = new Guid(entry.Id); + saveActivityCommand.GetParameter(index++).Value = entry.Name; + saveActivityCommand.GetParameter(index++).Value = entry.Overview; + saveActivityCommand.GetParameter(index++).Value = entry.ShortOverview; + saveActivityCommand.GetParameter(index++).Value = entry.Type; + saveActivityCommand.GetParameter(index++).Value = entry.ItemId; + saveActivityCommand.GetParameter(index++).Value = entry.UserId; + saveActivityCommand.GetParameter(index++).Value = entry.Date; + saveActivityCommand.GetParameter(index++).Value = entry.Severity.ToString(); + + saveActivityCommand.Transaction = transaction; + + saveActivityCommand.ExecuteNonQuery(); + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save record:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } + } + } + } + + public QueryResult GetActivityLogEntries(DateTime? minDate, int? startIndex, int? limit) + { + using (var connection = CreateConnection(true).Result) + { + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = BaseActivitySelectText; + + var whereClauses = new List(); + + if (minDate.HasValue) + { + whereClauses.Add("DateCreated>=@DateCreated"); + cmd.Parameters.Add(cmd, "@DateCreated", DbType.Date).Value = minDate.Value; + } + + var whereTextWithoutPaging = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + if (startIndex.HasValue && startIndex.Value > 0) + { + var pagingWhereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM ActivityLogEntries {0} ORDER BY DateCreated DESC LIMIT {1})", + pagingWhereText, + startIndex.Value.ToString(_usCulture))); + } + + var whereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + cmd.CommandText += whereText; + + cmd.CommandText += " ORDER BY DateCreated DESC"; + + if (limit.HasValue) + { + cmd.CommandText += " LIMIT " + limit.Value.ToString(_usCulture); + } + + cmd.CommandText += "; select count (Id) from ActivityLogEntries" + whereTextWithoutPaging; + + var list = new List(); + var count = 0; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) + { + while (reader.Read()) + { + list.Add(GetEntry(reader)); + } + + if (reader.NextResult() && reader.Read()) + { + count = reader.GetInt32(0); + } + } + + return new QueryResult() + { + Items = list.ToArray(), + TotalRecordCount = count + }; + } + } + } + + private ActivityLogEntry GetEntry(IDataReader reader) + { + var index = 0; + + var info = new ActivityLogEntry + { + Id = reader.GetGuid(index).ToString("N") + }; + + index++; + if (!reader.IsDBNull(index)) + { + info.Name = reader.GetString(index); + } + + index++; + if (!reader.IsDBNull(index)) + { + info.Overview = reader.GetString(index); + } + + index++; + if (!reader.IsDBNull(index)) + { + info.ShortOverview = reader.GetString(index); + } + + index++; + if (!reader.IsDBNull(index)) + { + info.Type = reader.GetString(index); + } + + index++; + if (!reader.IsDBNull(index)) + { + info.ItemId = reader.GetString(index); + } + + index++; + if (!reader.IsDBNull(index)) + { + info.UserId = reader.GetString(index); + } + + index++; + info.Date = reader.GetDateTime(index).ToUniversalTime(); + + index++; + if (!reader.IsDBNull(index)) + { + info.Severity = (LogSeverity)Enum.Parse(typeof(LogSeverity), reader.GetString(index), true); + } + + return info; + } + } +} diff --git a/Emby.Server.Core/Data/BaseSqliteRepository.cs b/Emby.Server.Core/Data/BaseSqliteRepository.cs new file mode 100644 index 0000000000..8d3cbe10a4 --- /dev/null +++ b/Emby.Server.Core/Data/BaseSqliteRepository.cs @@ -0,0 +1,114 @@ +using System; +using System.Data; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Logging; + +namespace Emby.Server.Core.Data +{ + public abstract class BaseSqliteRepository : IDisposable + { + protected SemaphoreSlim WriteLock = new SemaphoreSlim(1, 1); + protected readonly IDbConnector DbConnector; + protected ILogger Logger; + + protected string DbFilePath { get; set; } + + protected BaseSqliteRepository(ILogManager logManager, IDbConnector dbConnector) + { + DbConnector = dbConnector; + Logger = logManager.GetLogger(GetType().Name); + } + + protected virtual bool EnableConnectionPooling + { + get { return true; } + } + + protected virtual async Task CreateConnection(bool isReadOnly = false) + { + var connection = await DbConnector.Connect(DbFilePath, false, true).ConfigureAwait(false); + + connection.RunQueries(new[] + { + "pragma temp_store = memory" + + }, Logger); + + return connection; + } + + private bool _disposed; + protected void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name + " has been disposed and cannot be accessed."); + } + } + + public void Dispose() + { + _disposed = true; + Dispose(true); + GC.SuppressFinalize(this); + } + + protected async Task Vacuum(IDbConnection connection) + { + CheckDisposed(); + + await WriteLock.WaitAsync().ConfigureAwait(false); + + try + { + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "vacuum"; + cmd.ExecuteNonQuery(); + } + } + catch (Exception e) + { + Logger.ErrorException("Failed to vacuum:", e); + + throw; + } + finally + { + WriteLock.Release(); + } + } + + private readonly object _disposeLock = new object(); + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + try + { + lock (_disposeLock) + { + WriteLock.Wait(); + + CloseConnection(); + } + } + catch (Exception ex) + { + Logger.ErrorException("Error disposing database", ex); + } + } + } + + protected virtual void CloseConnection() + { + + } + } +} \ No newline at end of file diff --git a/Emby.Server.Core/Data/DataExtensions.cs b/Emby.Server.Core/Data/DataExtensions.cs new file mode 100644 index 0000000000..b633d92178 --- /dev/null +++ b/Emby.Server.Core/Data/DataExtensions.cs @@ -0,0 +1,181 @@ +using System; +using System.Data; +using System.IO; +using System.Text; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; + +namespace Emby.Server.Core.Data +{ + public static class DataExtensions + { + /// + /// Determines whether the specified conn is open. + /// + /// The conn. + /// true if the specified conn is open; otherwise, false. + public static bool IsOpen(this IDbConnection conn) + { + return conn.State == ConnectionState.Open; + } + + public static IDataParameter GetParameter(this IDbCommand cmd, int index) + { + return (IDataParameter)cmd.Parameters[index]; + } + + public static IDataParameter Add(this IDataParameterCollection paramCollection, IDbCommand cmd, string name, DbType type) + { + var param = cmd.CreateParameter(); + + param.ParameterName = name; + param.DbType = type; + + paramCollection.Add(param); + + return param; + } + + public static IDataParameter Add(this IDataParameterCollection paramCollection, IDbCommand cmd, string name) + { + var param = cmd.CreateParameter(); + + param.ParameterName = name; + + paramCollection.Add(param); + + return param; + } + + + /// + /// Gets a stream from a DataReader at a given ordinal + /// + /// Stream. + /// reader + public static Stream GetMemoryStream(this IDataReader reader, int ordinal, IMemoryStreamFactory streamProvider) + { + if (reader == null) + { + throw new ArgumentNullException("reader"); + } + + var memoryStream = streamProvider.CreateNew(); + var num = 0L; + var array = new byte[4096]; + long bytes; + do + { + bytes = reader.GetBytes(ordinal, num, array, 0, array.Length); + memoryStream.Write(array, 0, (int)bytes); + num += bytes; + } + while (bytes > 0L); + memoryStream.Position = 0; + return memoryStream; + } + + /// + /// Runs the queries. + /// + /// The connection. + /// The queries. + /// The logger. + /// true if XXXX, false otherwise + /// queries + public static void RunQueries(this IDbConnection connection, string[] queries, ILogger logger) + { + if (queries == null) + { + throw new ArgumentNullException("queries"); + } + + using (var tran = connection.BeginTransaction()) + { + try + { + using (var cmd = connection.CreateCommand()) + { + foreach (var query in queries) + { + cmd.Transaction = tran; + cmd.CommandText = query; + cmd.ExecuteNonQuery(); + } + } + + tran.Commit(); + } + catch (Exception e) + { + logger.ErrorException("Error running queries", e); + tran.Rollback(); + throw; + } + } + } + + public static void Attach(IDbConnection db, string path, string alias) + { + using (var cmd = db.CreateCommand()) + { + cmd.CommandText = string.Format("attach @dbPath as {0};", alias); + cmd.Parameters.Add(cmd, "@dbPath", DbType.String); + cmd.GetParameter(0).Value = path; + + cmd.ExecuteNonQuery(); + } + } + + /// + /// Serializes to bytes. + /// + /// System.Byte[][]. + /// obj + public static byte[] SerializeToBytes(this IJsonSerializer json, object obj, IMemoryStreamFactory streamProvider) + { + if (obj == null) + { + throw new ArgumentNullException("obj"); + } + + using (var stream = streamProvider.CreateNew()) + { + json.SerializeToStream(obj, stream); + return stream.ToArray(); + } + } + + public static void AddColumn(this IDbConnection connection, ILogger logger, string table, string columnName, string type) + { + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(" + table + ")"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, columnName, StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table " + table); + builder.AppendLine("add column " + columnName + " " + type); + + connection.RunQueries(new[] { builder.ToString() }, logger); + } + } +} \ No newline at end of file diff --git a/Emby.Server.Core/Data/IDbConnector.cs b/Emby.Server.Core/Data/IDbConnector.cs new file mode 100644 index 0000000000..ca6c13daee --- /dev/null +++ b/Emby.Server.Core/Data/IDbConnector.cs @@ -0,0 +1,10 @@ +using System.Data; +using System.Threading.Tasks; + +namespace Emby.Server.Core.Data +{ + public interface IDbConnector + { + Task Connect(string dbPath, bool isReadOnly, bool enablePooling = false, int? cacheSize = null); + } +} diff --git a/Emby.Server.Core/Data/MediaStreamColumns.cs b/Emby.Server.Core/Data/MediaStreamColumns.cs new file mode 100644 index 0000000000..cb46a56025 --- /dev/null +++ b/Emby.Server.Core/Data/MediaStreamColumns.cs @@ -0,0 +1,408 @@ +using System; +using System.Data; +using System.Text; +using MediaBrowser.Model.Logging; + +namespace Emby.Server.Core.Data +{ + public class MediaStreamColumns + { + private readonly IDbConnection _connection; + private readonly ILogger _logger; + + public MediaStreamColumns(IDbConnection connection, ILogger logger) + { + _connection = connection; + _logger = logger; + } + + public void AddColumns() + { + AddPixelFormatColumnCommand(); + AddBitDepthCommand(); + AddIsAnamorphicColumn(); + AddKeyFramesColumn(); + AddRefFramesCommand(); + AddCodecTagColumn(); + AddCommentColumn(); + AddNalColumn(); + AddIsAvcColumn(); + AddTitleColumn(); + AddTimeBaseColumn(); + AddCodecTimeBaseColumn(); + } + + private void AddIsAvcColumn() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "IsAvc", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column IsAvc BIT NULL"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); + } + + private void AddTimeBaseColumn() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "TimeBase", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column TimeBase TEXT"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); + } + + private void AddCodecTimeBaseColumn() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "CodecTimeBase", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column CodecTimeBase TEXT"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); + } + + private void AddTitleColumn() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "Title", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column Title TEXT"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); + } + + private void AddNalColumn() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "NalLengthSize", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column NalLengthSize TEXT"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); + } + + private void AddCommentColumn() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "Comment", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column Comment TEXT"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); + } + + private void AddCodecTagColumn() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "CodecTag", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column CodecTag TEXT"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); + } + + private void AddPixelFormatColumnCommand() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "PixelFormat", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column PixelFormat TEXT"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); + } + + private void AddBitDepthCommand() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "BitDepth", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column BitDepth INT NULL"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); + } + + private void AddRefFramesCommand() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "RefFrames", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column RefFrames INT NULL"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); + } + + private void AddKeyFramesColumn() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "KeyFrames", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column KeyFrames TEXT NULL"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); + } + + private void AddIsAnamorphicColumn() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "IsAnamorphic", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column IsAnamorphic BIT NULL"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); + } + + } +} diff --git a/Emby.Server.Core/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Core/Data/SqliteDisplayPreferencesRepository.cs new file mode 100644 index 0000000000..a9e63a11d0 --- /dev/null +++ b/Emby.Server.Core/Data/SqliteDisplayPreferencesRepository.cs @@ -0,0 +1,312 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; + +namespace Emby.Server.Core.Data +{ + /// + /// Class SQLiteDisplayPreferencesRepository + /// + public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository + { + private readonly IMemoryStreamFactory _memoryStreamProvider; + + public SqliteDisplayPreferencesRepository(ILogManager logManager, IJsonSerializer jsonSerializer, IApplicationPaths appPaths, IDbConnector dbConnector, IMemoryStreamFactory memoryStreamProvider) + : base(logManager, dbConnector) + { + _jsonSerializer = jsonSerializer; + _memoryStreamProvider = memoryStreamProvider; + DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db"); + } + + /// + /// Gets the name of the repository + /// + /// The name. + public string Name + { + get + { + return "SQLite"; + } + } + + /// + /// The _json serializer + /// + private readonly IJsonSerializer _jsonSerializer; + + /// + /// Opens the connection to the database + /// + /// Task. + public async Task Initialize() + { + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + string[] queries = { + + "create table if not exists userdisplaypreferences (id GUID, userId GUID, client text, data BLOB)", + "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)" + }; + + connection.RunQueries(queries, Logger); + } + } + + /// + /// Save the display preferences associated with an item in the repo + /// + /// The display preferences. + /// The user id. + /// The client. + /// The cancellation token. + /// Task. + /// item + public async Task SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, CancellationToken cancellationToken) + { + if (displayPreferences == null) + { + throw new ArgumentNullException("displayPreferences"); + } + if (string.IsNullOrWhiteSpace(displayPreferences.Id)) + { + throw new ArgumentNullException("displayPreferences.Id"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + var serialized = _jsonSerializer.SerializeToBytes(displayPreferences, _memoryStreamProvider); + + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + IDbTransaction transaction = null; + + try + { + transaction = connection.BeginTransaction(); + + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "replace into userdisplaypreferences (id, userid, client, data) values (@1, @2, @3, @4)"; + + cmd.Parameters.Add(cmd, "@1", DbType.Guid).Value = new Guid(displayPreferences.Id); + cmd.Parameters.Add(cmd, "@2", DbType.Guid).Value = userId; + cmd.Parameters.Add(cmd, "@3", DbType.String).Value = client; + cmd.Parameters.Add(cmd, "@4", DbType.Binary).Value = serialized; + + cmd.Transaction = transaction; + + cmd.ExecuteNonQuery(); + } + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save display preferences:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } + } + } + + /// + /// Save all display preferences associated with a user in the repo + /// + /// The display preferences. + /// The user id. + /// The cancellation token. + /// Task. + /// item + public async Task SaveAllDisplayPreferences(IEnumerable displayPreferences, Guid userId, CancellationToken cancellationToken) + { + if (displayPreferences == null) + { + throw new ArgumentNullException("displayPreferences"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + IDbTransaction transaction = null; + + try + { + transaction = connection.BeginTransaction(); + + foreach (var displayPreference in displayPreferences) + { + + var serialized = _jsonSerializer.SerializeToBytes(displayPreference, _memoryStreamProvider); + + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "replace into userdisplaypreferences (id, userid, client, data) values (@1, @2, @3, @4)"; + + cmd.Parameters.Add(cmd, "@1", DbType.Guid).Value = new Guid(displayPreference.Id); + cmd.Parameters.Add(cmd, "@2", DbType.Guid).Value = userId; + cmd.Parameters.Add(cmd, "@3", DbType.String).Value = displayPreference.Client; + cmd.Parameters.Add(cmd, "@4", DbType.Binary).Value = serialized; + + cmd.Transaction = transaction; + + cmd.ExecuteNonQuery(); + } + } + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save display preferences:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } + } + } + + /// + /// Gets the display preferences. + /// + /// The display preferences id. + /// The user id. + /// The client. + /// Task{DisplayPreferences}. + /// item + public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, Guid userId, string client) + { + if (string.IsNullOrWhiteSpace(displayPreferencesId)) + { + throw new ArgumentNullException("displayPreferencesId"); + } + + var guidId = displayPreferencesId.GetMD5(); + + using (var connection = CreateConnection(true).Result) + { + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "select data from userdisplaypreferences where id = @id and userId=@userId and client=@client"; + + cmd.Parameters.Add(cmd, "@id", DbType.Guid).Value = guidId; + cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId; + cmd.Parameters.Add(cmd, "@client", DbType.String).Value = client; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) + { + if (reader.Read()) + { + using (var stream = reader.GetMemoryStream(0, _memoryStreamProvider)) + { + return _jsonSerializer.DeserializeFromStream(stream); + } + } + } + + return new DisplayPreferences + { + Id = guidId.ToString("N") + }; + } + } + } + + /// + /// Gets all display preferences for the given user. + /// + /// The user id. + /// Task{DisplayPreferences}. + /// item + public IEnumerable GetAllDisplayPreferences(Guid userId) + { + var list = new List(); + + using (var connection = CreateConnection(true).Result) + { + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "select data from userdisplaypreferences where userId=@userId"; + + cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + using (var stream = reader.GetMemoryStream(0, _memoryStreamProvider)) + { + list.Add(_jsonSerializer.DeserializeFromStream(stream)); + } + } + } + } + } + + return list; + } + + public Task SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken) + { + return SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken); + } + + public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client) + { + return GetDisplayPreferences(displayPreferencesId, new Guid(userId), client); + } + } +} \ No newline at end of file diff --git a/Emby.Server.Core/Data/SqliteFileOrganizationRepository.cs b/Emby.Server.Core/Data/SqliteFileOrganizationRepository.cs new file mode 100644 index 0000000000..a858db28a7 --- /dev/null +++ b/Emby.Server.Core/Data/SqliteFileOrganizationRepository.cs @@ -0,0 +1,408 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.FileOrganization; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Querying; + +namespace Emby.Server.Core.Data +{ + public class SqliteFileOrganizationRepository : BaseSqliteRepository, IFileOrganizationRepository, IDisposable + { + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + public SqliteFileOrganizationRepository(ILogManager logManager, IServerApplicationPaths appPaths, IDbConnector connector) : base(logManager, connector) + { + DbFilePath = Path.Combine(appPaths.DataPath, "fileorganization.db"); + } + + /// + /// Opens the connection to the database + /// + /// Task. + public async Task Initialize() + { + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + string[] queries = { + + "create table if not exists FileOrganizerResults (ResultId GUID PRIMARY KEY, OriginalPath TEXT, TargetPath TEXT, FileLength INT, OrganizationDate datetime, Status TEXT, OrganizationType TEXT, StatusMessage TEXT, ExtractedName TEXT, ExtractedYear int null, ExtractedSeasonNumber int null, ExtractedEpisodeNumber int null, ExtractedEndingEpisodeNumber, DuplicatePaths TEXT int null)", + "create index if not exists idx_FileOrganizerResults on FileOrganizerResults(ResultId)" + }; + + connection.RunQueries(queries, Logger); + } + } + + public async Task SaveResult(FileOrganizationResult result, CancellationToken cancellationToken) + { + if (result == null) + { + throw new ArgumentNullException("result"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + using (var saveResultCommand = connection.CreateCommand()) + { + saveResultCommand.CommandText = "replace into FileOrganizerResults (ResultId, OriginalPath, TargetPath, FileLength, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber, DuplicatePaths) values (@ResultId, @OriginalPath, @TargetPath, @FileLength, @OrganizationDate, @Status, @OrganizationType, @StatusMessage, @ExtractedName, @ExtractedYear, @ExtractedSeasonNumber, @ExtractedEpisodeNumber, @ExtractedEndingEpisodeNumber, @DuplicatePaths)"; + + saveResultCommand.Parameters.Add(saveResultCommand, "@ResultId"); + saveResultCommand.Parameters.Add(saveResultCommand, "@OriginalPath"); + saveResultCommand.Parameters.Add(saveResultCommand, "@TargetPath"); + saveResultCommand.Parameters.Add(saveResultCommand, "@FileLength"); + saveResultCommand.Parameters.Add(saveResultCommand, "@OrganizationDate"); + saveResultCommand.Parameters.Add(saveResultCommand, "@Status"); + saveResultCommand.Parameters.Add(saveResultCommand, "@OrganizationType"); + saveResultCommand.Parameters.Add(saveResultCommand, "@StatusMessage"); + saveResultCommand.Parameters.Add(saveResultCommand, "@ExtractedName"); + saveResultCommand.Parameters.Add(saveResultCommand, "@ExtractedYear"); + saveResultCommand.Parameters.Add(saveResultCommand, "@ExtractedSeasonNumber"); + saveResultCommand.Parameters.Add(saveResultCommand, "@ExtractedEpisodeNumber"); + saveResultCommand.Parameters.Add(saveResultCommand, "@ExtractedEndingEpisodeNumber"); + saveResultCommand.Parameters.Add(saveResultCommand, "@DuplicatePaths"); + + IDbTransaction transaction = null; + + try + { + transaction = connection.BeginTransaction(); + + var index = 0; + + saveResultCommand.GetParameter(index++).Value = new Guid(result.Id); + saveResultCommand.GetParameter(index++).Value = result.OriginalPath; + saveResultCommand.GetParameter(index++).Value = result.TargetPath; + saveResultCommand.GetParameter(index++).Value = result.FileSize; + saveResultCommand.GetParameter(index++).Value = result.Date; + saveResultCommand.GetParameter(index++).Value = result.Status.ToString(); + saveResultCommand.GetParameter(index++).Value = result.Type.ToString(); + saveResultCommand.GetParameter(index++).Value = result.StatusMessage; + saveResultCommand.GetParameter(index++).Value = result.ExtractedName; + saveResultCommand.GetParameter(index++).Value = result.ExtractedYear; + saveResultCommand.GetParameter(index++).Value = result.ExtractedSeasonNumber; + saveResultCommand.GetParameter(index++).Value = result.ExtractedEpisodeNumber; + saveResultCommand.GetParameter(index++).Value = result.ExtractedEndingEpisodeNumber; + saveResultCommand.GetParameter(index).Value = string.Join("|", result.DuplicatePaths.ToArray()); + + saveResultCommand.Transaction = transaction; + + saveResultCommand.ExecuteNonQuery(); + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save FileOrganizationResult:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } + } + } + } + + public async Task Delete(string id) + { + if (string.IsNullOrEmpty(id)) + { + throw new ArgumentNullException("id"); + } + + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + using (var deleteResultCommand = connection.CreateCommand()) + { + deleteResultCommand.CommandText = "delete from FileOrganizerResults where ResultId = @ResultId"; + + deleteResultCommand.Parameters.Add(deleteResultCommand, "@ResultId"); + + IDbTransaction transaction = null; + + try + { + transaction = connection.BeginTransaction(); + + deleteResultCommand.GetParameter(0).Value = new Guid(id); + + deleteResultCommand.Transaction = transaction; + + deleteResultCommand.ExecuteNonQuery(); + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to delete FileOrganizationResult:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } + } + } + } + + public async Task DeleteAll() + { + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "delete from FileOrganizerResults"; + + IDbTransaction transaction = null; + + try + { + transaction = connection.BeginTransaction(); + + cmd.Transaction = transaction; + + cmd.ExecuteNonQuery(); + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to delete results", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } + } + } + } + + public QueryResult GetResults(FileOrganizationResultQuery query) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + using (var connection = CreateConnection(true).Result) + { + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "SELECT ResultId, OriginalPath, TargetPath, FileLength, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber, DuplicatePaths from FileOrganizerResults"; + + if (query.StartIndex.HasValue && query.StartIndex.Value > 0) + { + cmd.CommandText += string.Format(" WHERE ResultId NOT IN (SELECT ResultId FROM FileOrganizerResults ORDER BY OrganizationDate desc LIMIT {0})", + query.StartIndex.Value.ToString(_usCulture)); + } + + cmd.CommandText += " ORDER BY OrganizationDate desc"; + + if (query.Limit.HasValue) + { + cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); + } + + cmd.CommandText += "; select count (ResultId) from FileOrganizerResults"; + + var list = new List(); + var count = 0; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) + { + while (reader.Read()) + { + list.Add(GetResult(reader)); + } + + if (reader.NextResult() && reader.Read()) + { + count = reader.GetInt32(0); + } + } + + return new QueryResult() + { + Items = list.ToArray(), + TotalRecordCount = count + }; + } + } + } + + public FileOrganizationResult GetResult(string id) + { + if (string.IsNullOrEmpty(id)) + { + throw new ArgumentNullException("id"); + } + + using (var connection = CreateConnection(true).Result) + { + var guid = new Guid(id); + + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "select ResultId, OriginalPath, TargetPath, FileLength, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber, DuplicatePaths from FileOrganizerResults where ResultId=@Id"; + + cmd.Parameters.Add(cmd, "@Id", DbType.Guid).Value = guid; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) + { + if (reader.Read()) + { + return GetResult(reader); + } + } + } + + return null; + } + } + + public FileOrganizationResult GetResult(IDataReader reader) + { + var index = 0; + + var result = new FileOrganizationResult + { + Id = reader.GetGuid(0).ToString("N") + }; + + index++; + if (!reader.IsDBNull(index)) + { + result.OriginalPath = reader.GetString(index); + } + + index++; + if (!reader.IsDBNull(index)) + { + result.TargetPath = reader.GetString(index); + } + + index++; + result.FileSize = reader.GetInt64(index); + + index++; + result.Date = reader.GetDateTime(index).ToUniversalTime(); + + index++; + result.Status = (FileSortingStatus)Enum.Parse(typeof(FileSortingStatus), reader.GetString(index), true); + + index++; + result.Type = (FileOrganizerType)Enum.Parse(typeof(FileOrganizerType), reader.GetString(index), true); + + index++; + if (!reader.IsDBNull(index)) + { + result.StatusMessage = reader.GetString(index); + } + + result.OriginalFileName = Path.GetFileName(result.OriginalPath); + + index++; + if (!reader.IsDBNull(index)) + { + result.ExtractedName = reader.GetString(index); + } + + index++; + if (!reader.IsDBNull(index)) + { + result.ExtractedYear = reader.GetInt32(index); + } + + index++; + if (!reader.IsDBNull(index)) + { + result.ExtractedSeasonNumber = reader.GetInt32(index); + } + + index++; + if (!reader.IsDBNull(index)) + { + result.ExtractedEpisodeNumber = reader.GetInt32(index); + } + + index++; + if (!reader.IsDBNull(index)) + { + result.ExtractedEndingEpisodeNumber = reader.GetInt32(index); + } + + index++; + if (!reader.IsDBNull(index)) + { + result.DuplicatePaths = reader.GetString(index).Split('|').Where(i => !string.IsNullOrEmpty(i)).ToList(); + } + + return result; + } + } +} diff --git a/Emby.Server.Core/Data/SqliteItemRepository.cs b/Emby.Server.Core/Data/SqliteItemRepository.cs new file mode 100644 index 0000000000..2ca86c8318 --- /dev/null +++ b/Emby.Server.Core/Data/SqliteItemRepository.cs @@ -0,0 +1,5423 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Collections; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.LiveTv; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Server.Implementations.Devices; +using MediaBrowser.Server.Implementations.Playlists; + +namespace Emby.Server.Core.Data +{ + /// + /// Class SQLiteItemRepository + /// + public class SqliteItemRepository : BaseSqliteRepository, IItemRepository + { + private IDbConnection _connection; + + private readonly TypeMapper _typeMapper = new TypeMapper(); + + /// + /// Gets the name of the repository + /// + /// The name. + public string Name + { + get + { + return "SQLite"; + } + } + + /// + /// Gets the json serializer. + /// + /// The json serializer. + private readonly IJsonSerializer _jsonSerializer; + + /// + /// The _app paths + /// + private readonly IServerConfigurationManager _config; + + /// + /// The _save item command + /// + private IDbCommand _saveItemCommand; + + private readonly string _criticReviewsPath; + + private IDbCommand _deleteItemCommand; + + private IDbCommand _deletePeopleCommand; + private IDbCommand _savePersonCommand; + + private IDbCommand _deleteChaptersCommand; + private IDbCommand _saveChapterCommand; + + private IDbCommand _deleteStreamsCommand; + private IDbCommand _saveStreamCommand; + + private IDbCommand _deleteAncestorsCommand; + private IDbCommand _saveAncestorCommand; + + private IDbCommand _deleteItemValuesCommand; + private IDbCommand _saveItemValuesCommand; + + private IDbCommand _deleteProviderIdsCommand; + private IDbCommand _saveProviderIdsCommand; + + private IDbCommand _deleteImagesCommand; + private IDbCommand _saveImagesCommand; + + private IDbCommand _updateInheritedTagsCommand; + + public const int LatestSchemaVersion = 109; + private readonly IMemoryStreamFactory _memoryStreamProvider; + + /// + /// Initializes a new instance of the class. + /// + public SqliteItemRepository(IServerConfigurationManager config, IJsonSerializer jsonSerializer, ILogManager logManager, IDbConnector connector, IMemoryStreamFactory memoryStreamProvider) + : base(logManager, connector) + { + if (config == null) + { + throw new ArgumentNullException("config"); + } + if (jsonSerializer == null) + { + throw new ArgumentNullException("jsonSerializer"); + } + + _config = config; + _jsonSerializer = jsonSerializer; + _memoryStreamProvider = memoryStreamProvider; + + _criticReviewsPath = Path.Combine(_config.ApplicationPaths.DataPath, "critic-reviews"); + DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db"); + } + + private const string ChaptersTableName = "Chapters2"; + + protected override async Task CreateConnection(bool isReadOnly = false) + { + var cacheSize = _config.Configuration.SqliteCacheSize; + if (cacheSize <= 0) + { + cacheSize = Math.Min(Environment.ProcessorCount * 50000, 100000); + } + + var connection = await DbConnector.Connect(DbFilePath, false, false, 0 - cacheSize).ConfigureAwait(false); + + connection.RunQueries(new[] + { + "pragma temp_store = memory", + "pragma default_temp_store = memory", + "PRAGMA locking_mode=EXCLUSIVE" + + }, Logger); + + return connection; + } + + /// + /// Opens the connection to the database + /// + /// Task. + public async Task Initialize(SqliteUserDataRepository userDataRepo) + { + _connection = await CreateConnection(false).ConfigureAwait(false); + + var createMediaStreamsTableCommand + = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, PRIMARY KEY (ItemId, StreamIndex))"; + + string[] queries = { + + "create table if not exists TypedBaseItems (guid GUID primary key, type TEXT, data BLOB, ParentId GUID, Path TEXT)", + + "create table if not exists AncestorIds (ItemId GUID, AncestorId GUID, AncestorIdText TEXT, PRIMARY KEY (ItemId, AncestorId))", + "create index if not exists idx_AncestorIds1 on AncestorIds(AncestorId)", + "create index if not exists idx_AncestorIds2 on AncestorIds(AncestorIdText)", + + "create table if not exists ItemValues (ItemId GUID, Type INT, Value TEXT, CleanValue TEXT)", + + "create table if not exists ProviderIds (ItemId GUID, Name TEXT, Value TEXT, PRIMARY KEY (ItemId, Name))", + // covering index + "create index if not exists Idx_ProviderIds1 on ProviderIds(ItemId,Name,Value)", + + "create table if not exists Images (ItemId GUID NOT NULL, Path TEXT NOT NULL, ImageType INT NOT NULL, DateModified DATETIME, IsPlaceHolder BIT NOT NULL, SortOrder INT)", + "create index if not exists idx_Images on Images(ItemId)", + + "create table if not exists People (ItemId GUID, Name TEXT NOT NULL, Role TEXT, PersonType TEXT, SortOrder int, ListOrder int)", + + "drop index if exists idxPeopleItemId", + "create index if not exists idxPeopleItemId1 on People(ItemId,ListOrder)", + "create index if not exists idxPeopleName on People(Name)", + + "create table if not exists "+ChaptersTableName+" (ItemId GUID, ChapterIndex INT, StartPositionTicks BIGINT, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))", + + createMediaStreamsTableCommand, + + "create index if not exists idx_mediastreams1 on mediastreams(ItemId)", + + //"drop table if exists UserDataKeys" + + }; + + _connection.RunQueries(queries, Logger); + + _connection.AddColumn(Logger, "AncestorIds", "AncestorIdText", "Text"); + + _connection.AddColumn(Logger, "TypedBaseItems", "Path", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "StartDate", "DATETIME"); + _connection.AddColumn(Logger, "TypedBaseItems", "EndDate", "DATETIME"); + _connection.AddColumn(Logger, "TypedBaseItems", "ChannelId", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsMovie", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsSports", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsKids", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "CommunityRating", "Float"); + _connection.AddColumn(Logger, "TypedBaseItems", "CustomRating", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "IndexNumber", "INT"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsLocked", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "Name", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "OfficialRating", "Text"); + + _connection.AddColumn(Logger, "TypedBaseItems", "MediaType", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "Overview", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "ParentIndexNumber", "INT"); + _connection.AddColumn(Logger, "TypedBaseItems", "PremiereDate", "DATETIME"); + _connection.AddColumn(Logger, "TypedBaseItems", "ProductionYear", "INT"); + _connection.AddColumn(Logger, "TypedBaseItems", "ParentId", "GUID"); + _connection.AddColumn(Logger, "TypedBaseItems", "Genres", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "SchemaVersion", "INT"); + _connection.AddColumn(Logger, "TypedBaseItems", "SortName", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "RunTimeTicks", "BIGINT"); + + _connection.AddColumn(Logger, "TypedBaseItems", "OfficialRatingDescription", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "HomePageUrl", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "VoteCount", "INT"); + _connection.AddColumn(Logger, "TypedBaseItems", "DisplayMediaType", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "DateCreated", "DATETIME"); + _connection.AddColumn(Logger, "TypedBaseItems", "DateModified", "DATETIME"); + + _connection.AddColumn(Logger, "TypedBaseItems", "ForcedSortName", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsOffline", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "LocationType", "Text"); + + _connection.AddColumn(Logger, "TypedBaseItems", "IsSeries", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsLive", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsNews", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsPremiere", "BIT"); + + _connection.AddColumn(Logger, "TypedBaseItems", "EpisodeTitle", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsRepeat", "BIT"); + + _connection.AddColumn(Logger, "TypedBaseItems", "PreferredMetadataLanguage", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "PreferredMetadataCountryCode", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsHD", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "ExternalEtag", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "DateLastRefreshed", "DATETIME"); + + _connection.AddColumn(Logger, "TypedBaseItems", "DateLastSaved", "DATETIME"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsInMixedFolder", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "LockedFields", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "Studios", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "Audio", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "ExternalServiceId", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "Tags", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsFolder", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "InheritedParentalRatingValue", "INT"); + _connection.AddColumn(Logger, "TypedBaseItems", "UnratedType", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "TopParentId", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsItemByName", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "SourceType", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "TrailerTypes", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "CriticRating", "Float"); + _connection.AddColumn(Logger, "TypedBaseItems", "CriticRatingSummary", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "InheritedTags", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "CleanName", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "PresentationUniqueKey", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "SlugName", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "OriginalTitle", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "PrimaryVersionId", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "DateLastMediaAdded", "DATETIME"); + _connection.AddColumn(Logger, "TypedBaseItems", "Album", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsVirtualItem", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "SeriesName", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "UserDataKey", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "SeasonName", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "SeasonId", "GUID"); + _connection.AddColumn(Logger, "TypedBaseItems", "SeriesId", "GUID"); + _connection.AddColumn(Logger, "TypedBaseItems", "SeriesSortName", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "ExternalSeriesId", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "ShortOverview", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "Tagline", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "Keywords", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "ProviderIds", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "Images", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "ProductionLocations", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "ThemeSongIds", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "ThemeVideoIds", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "TotalBitrate", "INT"); + _connection.AddColumn(Logger, "TypedBaseItems", "ExtraType", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "Artists", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "AlbumArtists", "Text"); + + _connection.AddColumn(Logger, "ItemValues", "CleanValue", "Text"); + + _connection.AddColumn(Logger, ChaptersTableName, "ImageDateModified", "DATETIME"); + + string[] postQueries = + + { + // obsolete + "drop index if exists idx_TypedBaseItems", + "drop index if exists idx_mediastreams", + "drop index if exists idx_"+ChaptersTableName, + "drop index if exists idx_UserDataKeys1", + "drop index if exists idx_UserDataKeys2", + "drop index if exists idx_TypeTopParentId3", + "drop index if exists idx_TypeTopParentId2", + "drop index if exists idx_TypeTopParentId4", + "drop index if exists idx_Type", + "drop index if exists idx_TypeTopParentId", + "drop index if exists idx_GuidType", + "drop index if exists idx_TopParentId", + "drop index if exists idx_TypeTopParentId6", + "drop index if exists idx_ItemValues2", + "drop index if exists Idx_ProviderIds", + "drop index if exists idx_ItemValues3", + "drop index if exists idx_ItemValues4", + "drop index if exists idx_ItemValues5", + "drop index if exists idx_UserDataKeys3", + "drop table if exists UserDataKeys", + + "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)", + "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)", + + "create index if not exists idx_PresentationUniqueKey on TypedBaseItems(PresentationUniqueKey)", + "create index if not exists idx_GuidTypeIsFolderIsVirtualItem on TypedBaseItems(Guid,Type,IsFolder,IsVirtualItem)", + //"create index if not exists idx_GuidMediaTypeIsFolderIsVirtualItem on TypedBaseItems(Guid,MediaType,IsFolder,IsVirtualItem)", + "create index if not exists idx_CleanNameType on TypedBaseItems(CleanName,Type)", + + // covering index + "create index if not exists idx_TopParentIdGuid on TypedBaseItems(TopParentId,Guid)", + + // live tv programs + "create index if not exists idx_TypeTopParentIdStartDate on TypedBaseItems(Type,TopParentId,StartDate)", + + // covering index for getitemvalues + "create index if not exists idx_TypeTopParentIdGuid on TypedBaseItems(Type,TopParentId,Guid)", + + // used by movie suggestions + "create index if not exists idx_TypeTopParentIdGroup on TypedBaseItems(Type,TopParentId,PresentationUniqueKey)", + "create index if not exists idx_TypeTopParentId5 on TypedBaseItems(TopParentId,IsVirtualItem)", + + // latest items + "create index if not exists idx_TypeTopParentId9 on TypedBaseItems(TopParentId,Type,IsVirtualItem,PresentationUniqueKey,DateCreated)", + "create index if not exists idx_TypeTopParentId8 on TypedBaseItems(TopParentId,IsFolder,IsVirtualItem,PresentationUniqueKey,DateCreated)", + + // resume + "create index if not exists idx_TypeTopParentId7 on TypedBaseItems(TopParentId,MediaType,IsVirtualItem,PresentationUniqueKey)", + + // items by name + "create index if not exists idx_ItemValues6 on ItemValues(ItemId,Type,CleanValue)", + "create index if not exists idx_ItemValues7 on ItemValues(Type,CleanValue,ItemId)" + }; + + _connection.RunQueries(postQueries, Logger); + + PrepareStatements(); + + new MediaStreamColumns(_connection, Logger).AddColumns(); + + DataExtensions.Attach(_connection, Path.Combine(_config.ApplicationPaths.DataPath, "userdata_v2.db"), "UserDataDb"); + await userDataRepo.Initialize(_connection, WriteLock).ConfigureAwait(false); + //await Vacuum(_connection).ConfigureAwait(false); + } + + private readonly string[] _retriveItemColumns = + { + "type", + "data", + "StartDate", + "EndDate", + "IsOffline", + "ChannelId", + "IsMovie", + "IsSports", + "IsKids", + "IsSeries", + "IsLive", + "IsNews", + "IsPremiere", + "EpisodeTitle", + "IsRepeat", + "CommunityRating", + "CustomRating", + "IndexNumber", + "IsLocked", + "PreferredMetadataLanguage", + "PreferredMetadataCountryCode", + "IsHD", + "ExternalEtag", + "DateLastRefreshed", + "Name", + "Path", + "PremiereDate", + "Overview", + "ParentIndexNumber", + "ProductionYear", + "OfficialRating", + "OfficialRatingDescription", + "HomePageUrl", + "DisplayMediaType", + "ForcedSortName", + "RunTimeTicks", + "VoteCount", + "DateCreated", + "DateModified", + "guid", + "Genres", + "ParentId", + "Audio", + "ExternalServiceId", + "IsInMixedFolder", + "DateLastSaved", + "LockedFields", + "Studios", + "Tags", + "SourceType", + "TrailerTypes", + "OriginalTitle", + "PrimaryVersionId", + "DateLastMediaAdded", + "Album", + "CriticRating", + "CriticRatingSummary", + "IsVirtualItem", + "SeriesName", + "SeasonName", + "SeasonId", + "SeriesId", + "SeriesSortName", + "PresentationUniqueKey", + "InheritedParentalRatingValue", + "InheritedTags", + "ExternalSeriesId", + "ShortOverview", + "Tagline", + "Keywords", + "ProviderIds", + "Images", + "ProductionLocations", + "ThemeSongIds", + "ThemeVideoIds", + "TotalBitrate", + "ExtraType", + "Artists", + "AlbumArtists" + }; + + private readonly string[] _mediaStreamSaveColumns = + { + "ItemId", + "StreamIndex", + "StreamType", + "Codec", + "Language", + "ChannelLayout", + "Profile", + "AspectRatio", + "Path", + "IsInterlaced", + "BitRate", + "Channels", + "SampleRate", + "IsDefault", + "IsForced", + "IsExternal", + "Height", + "Width", + "AverageFrameRate", + "RealFrameRate", + "Level", + "PixelFormat", + "BitDepth", + "IsAnamorphic", + "RefFrames", + "CodecTag", + "Comment", + "NalLengthSize", + "IsAvc", + "Title", + "TimeBase", + "CodecTimeBase" + }; + + /// + /// Prepares the statements. + /// + private void PrepareStatements() + { + var saveColumns = new List + { + "guid", + "type", + "data", + "Path", + "StartDate", + "EndDate", + "ChannelId", + "IsKids", + "IsMovie", + "IsSports", + "IsSeries", + "IsLive", + "IsNews", + "IsPremiere", + "EpisodeTitle", + "IsRepeat", + "CommunityRating", + "CustomRating", + "IndexNumber", + "IsLocked", + "Name", + "OfficialRating", + "MediaType", + "Overview", + "ParentIndexNumber", + "PremiereDate", + "ProductionYear", + "ParentId", + "Genres", + "InheritedParentalRatingValue", + "SchemaVersion", + "SortName", + "RunTimeTicks", + "OfficialRatingDescription", + "HomePageUrl", + "VoteCount", + "DisplayMediaType", + "DateCreated", + "DateModified", + "ForcedSortName", + "IsOffline", + "LocationType", + "PreferredMetadataLanguage", + "PreferredMetadataCountryCode", + "IsHD", + "ExternalEtag", + "DateLastRefreshed", + "DateLastSaved", + "IsInMixedFolder", + "LockedFields", + "Studios", + "Audio", + "ExternalServiceId", + "Tags", + "IsFolder", + "UnratedType", + "TopParentId", + "IsItemByName", + "SourceType", + "TrailerTypes", + "CriticRating", + "CriticRatingSummary", + "InheritedTags", + "CleanName", + "PresentationUniqueKey", + "SlugName", + "OriginalTitle", + "PrimaryVersionId", + "DateLastMediaAdded", + "Album", + "IsVirtualItem", + "SeriesName", + "UserDataKey", + "SeasonName", + "SeasonId", + "SeriesId", + "SeriesSortName", + "ExternalSeriesId", + "ShortOverview", + "Tagline", + "Keywords", + "ProviderIds", + "Images", + "ProductionLocations", + "ThemeSongIds", + "ThemeVideoIds", + "TotalBitrate", + "ExtraType", + "Artists", + "AlbumArtists" + }; + _saveItemCommand = _connection.CreateCommand(); + _saveItemCommand.CommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns.ToArray()) + ") values ("; + + for (var i = 1; i <= saveColumns.Count; i++) + { + if (i > 1) + { + _saveItemCommand.CommandText += ","; + } + _saveItemCommand.CommandText += "@" + i.ToString(CultureInfo.InvariantCulture); + + _saveItemCommand.Parameters.Add(_saveItemCommand, "@" + i.ToString(CultureInfo.InvariantCulture)); + } + _saveItemCommand.CommandText += ")"; + + _deleteItemCommand = _connection.CreateCommand(); + _deleteItemCommand.CommandText = "delete from TypedBaseItems where guid=@Id"; + _deleteItemCommand.Parameters.Add(_deleteItemCommand, "@Id"); + + // People + _deletePeopleCommand = _connection.CreateCommand(); + _deletePeopleCommand.CommandText = "delete from People where ItemId=@Id"; + _deletePeopleCommand.Parameters.Add(_deletePeopleCommand, "@Id"); + + _savePersonCommand = _connection.CreateCommand(); + _savePersonCommand.CommandText = "insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values (@ItemId, @Name, @Role, @PersonType, @SortOrder, @ListOrder)"; + _savePersonCommand.Parameters.Add(_savePersonCommand, "@ItemId"); + _savePersonCommand.Parameters.Add(_savePersonCommand, "@Name"); + _savePersonCommand.Parameters.Add(_savePersonCommand, "@Role"); + _savePersonCommand.Parameters.Add(_savePersonCommand, "@PersonType"); + _savePersonCommand.Parameters.Add(_savePersonCommand, "@SortOrder"); + _savePersonCommand.Parameters.Add(_savePersonCommand, "@ListOrder"); + + // Ancestors + _deleteAncestorsCommand = _connection.CreateCommand(); + _deleteAncestorsCommand.CommandText = "delete from AncestorIds where ItemId=@Id"; + _deleteAncestorsCommand.Parameters.Add(_deleteAncestorsCommand, "@Id"); + + _saveAncestorCommand = _connection.CreateCommand(); + _saveAncestorCommand.CommandText = "insert into AncestorIds (ItemId, AncestorId, AncestorIdText) values (@ItemId, @AncestorId, @AncestorIdText)"; + _saveAncestorCommand.Parameters.Add(_saveAncestorCommand, "@ItemId"); + _saveAncestorCommand.Parameters.Add(_saveAncestorCommand, "@AncestorId"); + _saveAncestorCommand.Parameters.Add(_saveAncestorCommand, "@AncestorIdText"); + + // Chapters + _deleteChaptersCommand = _connection.CreateCommand(); + _deleteChaptersCommand.CommandText = "delete from " + ChaptersTableName + " where ItemId=@ItemId"; + _deleteChaptersCommand.Parameters.Add(_deleteChaptersCommand, "@ItemId"); + + _saveChapterCommand = _connection.CreateCommand(); + _saveChapterCommand.CommandText = "replace into " + ChaptersTableName + " (ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath) values (@ItemId, @ChapterIndex, @StartPositionTicks, @Name, @ImagePath)"; + + _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@ItemId"); + _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@ChapterIndex"); + _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@StartPositionTicks"); + _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@Name"); + _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@ImagePath"); + _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@ImageDateModified"); + + // MediaStreams + _deleteStreamsCommand = _connection.CreateCommand(); + _deleteStreamsCommand.CommandText = "delete from mediastreams where ItemId=@ItemId"; + _deleteStreamsCommand.Parameters.Add(_deleteStreamsCommand, "@ItemId"); + + _saveStreamCommand = _connection.CreateCommand(); + + _saveStreamCommand.CommandText = string.Format("replace into mediastreams ({0}) values ({1})", + string.Join(",", _mediaStreamSaveColumns), + string.Join(",", _mediaStreamSaveColumns.Select(i => "@" + i).ToArray())); + + foreach (var col in _mediaStreamSaveColumns) + { + _saveStreamCommand.Parameters.Add(_saveStreamCommand, "@" + col); + } + + _updateInheritedTagsCommand = _connection.CreateCommand(); + _updateInheritedTagsCommand.CommandText = "Update TypedBaseItems set InheritedTags=@InheritedTags where Guid=@Guid"; + _updateInheritedTagsCommand.Parameters.Add(_updateInheritedTagsCommand, "@Guid"); + _updateInheritedTagsCommand.Parameters.Add(_updateInheritedTagsCommand, "@InheritedTags"); + + // item values + _deleteItemValuesCommand = _connection.CreateCommand(); + _deleteItemValuesCommand.CommandText = "delete from ItemValues where ItemId=@Id"; + _deleteItemValuesCommand.Parameters.Add(_deleteItemValuesCommand, "@Id"); + + _saveItemValuesCommand = _connection.CreateCommand(); + _saveItemValuesCommand.CommandText = "insert into ItemValues (ItemId, Type, Value, CleanValue) values (@ItemId, @Type, @Value, @CleanValue)"; + _saveItemValuesCommand.Parameters.Add(_saveItemValuesCommand, "@ItemId"); + _saveItemValuesCommand.Parameters.Add(_saveItemValuesCommand, "@Type"); + _saveItemValuesCommand.Parameters.Add(_saveItemValuesCommand, "@Value"); + _saveItemValuesCommand.Parameters.Add(_saveItemValuesCommand, "@CleanValue"); + + // provider ids + _deleteProviderIdsCommand = _connection.CreateCommand(); + _deleteProviderIdsCommand.CommandText = "delete from ProviderIds where ItemId=@Id"; + _deleteProviderIdsCommand.Parameters.Add(_deleteProviderIdsCommand, "@Id"); + + _saveProviderIdsCommand = _connection.CreateCommand(); + _saveProviderIdsCommand.CommandText = "insert into ProviderIds (ItemId, Name, Value) values (@ItemId, @Name, @Value)"; + _saveProviderIdsCommand.Parameters.Add(_saveProviderIdsCommand, "@ItemId"); + _saveProviderIdsCommand.Parameters.Add(_saveProviderIdsCommand, "@Name"); + _saveProviderIdsCommand.Parameters.Add(_saveProviderIdsCommand, "@Value"); + + // images + _deleteImagesCommand = _connection.CreateCommand(); + _deleteImagesCommand.CommandText = "delete from Images where ItemId=@Id"; + _deleteImagesCommand.Parameters.Add(_deleteImagesCommand, "@Id"); + + _saveImagesCommand = _connection.CreateCommand(); + _saveImagesCommand.CommandText = "insert into Images (ItemId, ImageType, Path, DateModified, IsPlaceHolder, SortOrder) values (@ItemId, @ImageType, @Path, @DateModified, @IsPlaceHolder, @SortOrder)"; + _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@ItemId"); + _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@ImageType"); + _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@Path"); + _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@DateModified"); + _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@IsPlaceHolder"); + _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@SortOrder"); + } + + /// + /// Save a standard item in the repo + /// + /// The item. + /// The cancellation token. + /// Task. + /// item + public Task SaveItem(BaseItem item, CancellationToken cancellationToken) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + + return SaveItems(new[] { item }, cancellationToken); + } + + /// + /// Saves the items. + /// + /// The items. + /// The cancellation token. + /// Task. + /// + /// items + /// or + /// cancellationToken + /// + public async Task SaveItems(IEnumerable items, CancellationToken cancellationToken) + { + if (items == null) + { + throw new ArgumentNullException("items"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + CheckDisposed(); + + await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + IDbTransaction transaction = null; + + try + { + transaction = _connection.BeginTransaction(); + + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + + var index = 0; + + _saveItemCommand.GetParameter(index++).Value = item.Id; + _saveItemCommand.GetParameter(index++).Value = item.GetType().FullName; + + if (TypeRequiresDeserialization(item.GetType())) + { + _saveItemCommand.GetParameter(index++).Value = _jsonSerializer.SerializeToBytes(item, _memoryStreamProvider); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + + _saveItemCommand.GetParameter(index++).Value = item.Path; + + var hasStartDate = item as IHasStartDate; + if (hasStartDate != null) + { + _saveItemCommand.GetParameter(index++).Value = hasStartDate.StartDate; + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + + _saveItemCommand.GetParameter(index++).Value = item.EndDate; + _saveItemCommand.GetParameter(index++).Value = item.ChannelId; + + var hasProgramAttributes = item as IHasProgramAttributes; + if (hasProgramAttributes != null) + { + _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsKids; + _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsMovie; + _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsSports; + _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsSeries; + _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsLive; + _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsNews; + _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsPremiere; + _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.EpisodeTitle; + _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsRepeat; + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + _saveItemCommand.GetParameter(index++).Value = null; + _saveItemCommand.GetParameter(index++).Value = null; + _saveItemCommand.GetParameter(index++).Value = null; + _saveItemCommand.GetParameter(index++).Value = null; + _saveItemCommand.GetParameter(index++).Value = null; + _saveItemCommand.GetParameter(index++).Value = null; + _saveItemCommand.GetParameter(index++).Value = null; + _saveItemCommand.GetParameter(index++).Value = null; + } + + _saveItemCommand.GetParameter(index++).Value = item.CommunityRating; + _saveItemCommand.GetParameter(index++).Value = item.CustomRating; + + _saveItemCommand.GetParameter(index++).Value = item.IndexNumber; + _saveItemCommand.GetParameter(index++).Value = item.IsLocked; + + _saveItemCommand.GetParameter(index++).Value = item.Name; + _saveItemCommand.GetParameter(index++).Value = item.OfficialRating; + + _saveItemCommand.GetParameter(index++).Value = item.MediaType; + _saveItemCommand.GetParameter(index++).Value = item.Overview; + _saveItemCommand.GetParameter(index++).Value = item.ParentIndexNumber; + _saveItemCommand.GetParameter(index++).Value = item.PremiereDate; + _saveItemCommand.GetParameter(index++).Value = item.ProductionYear; + + if (item.ParentId == Guid.Empty) + { + _saveItemCommand.GetParameter(index++).Value = null; + } + else + { + _saveItemCommand.GetParameter(index++).Value = item.ParentId; + } + + _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.Genres.ToArray()); + _saveItemCommand.GetParameter(index++).Value = item.GetInheritedParentalRatingValue() ?? 0; + + _saveItemCommand.GetParameter(index++).Value = LatestSchemaVersion; + _saveItemCommand.GetParameter(index++).Value = item.SortName; + _saveItemCommand.GetParameter(index++).Value = item.RunTimeTicks; + + _saveItemCommand.GetParameter(index++).Value = item.OfficialRatingDescription; + _saveItemCommand.GetParameter(index++).Value = item.HomePageUrl; + _saveItemCommand.GetParameter(index++).Value = item.VoteCount; + _saveItemCommand.GetParameter(index++).Value = item.DisplayMediaType; + _saveItemCommand.GetParameter(index++).Value = item.DateCreated; + _saveItemCommand.GetParameter(index++).Value = item.DateModified; + + _saveItemCommand.GetParameter(index++).Value = item.ForcedSortName; + _saveItemCommand.GetParameter(index++).Value = item.IsOffline; + _saveItemCommand.GetParameter(index++).Value = item.LocationType.ToString(); + + _saveItemCommand.GetParameter(index++).Value = item.PreferredMetadataLanguage; + _saveItemCommand.GetParameter(index++).Value = item.PreferredMetadataCountryCode; + _saveItemCommand.GetParameter(index++).Value = item.IsHD; + _saveItemCommand.GetParameter(index++).Value = item.ExternalEtag; + + if (item.DateLastRefreshed == default(DateTime)) + { + _saveItemCommand.GetParameter(index++).Value = null; + } + else + { + _saveItemCommand.GetParameter(index++).Value = item.DateLastRefreshed; + } + + if (item.DateLastSaved == default(DateTime)) + { + _saveItemCommand.GetParameter(index++).Value = null; + } + else + { + _saveItemCommand.GetParameter(index++).Value = item.DateLastSaved; + } + + _saveItemCommand.GetParameter(index++).Value = item.IsInMixedFolder; + _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.LockedFields.Select(i => i.ToString()).ToArray()); + _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.Studios.ToArray()); + + if (item.Audio.HasValue) + { + _saveItemCommand.GetParameter(index++).Value = item.Audio.Value.ToString(); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + + _saveItemCommand.GetParameter(index++).Value = item.ServiceName; + + if (item.Tags.Count > 0) + { + _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.Tags.ToArray()); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + + _saveItemCommand.GetParameter(index++).Value = item.IsFolder; + + _saveItemCommand.GetParameter(index++).Value = item.GetBlockUnratedType().ToString(); + + var topParent = item.GetTopParent(); + if (topParent != null) + { + //Logger.Debug("Item {0} has top parent {1}", item.Id, topParent.Id); + _saveItemCommand.GetParameter(index++).Value = topParent.Id.ToString("N"); + } + else + { + //Logger.Debug("Item {0} has null top parent", item.Id); + _saveItemCommand.GetParameter(index++).Value = null; + } + + var isByName = false; + var byName = item as IItemByName; + if (byName != null) + { + var dualAccess = item as IHasDualAccess; + isByName = dualAccess == null || dualAccess.IsAccessedByName; + } + _saveItemCommand.GetParameter(index++).Value = isByName; + + _saveItemCommand.GetParameter(index++).Value = item.SourceType.ToString(); + + var trailer = item as Trailer; + if (trailer != null && trailer.TrailerTypes.Count > 0) + { + _saveItemCommand.GetParameter(index++).Value = string.Join("|", trailer.TrailerTypes.Select(i => i.ToString()).ToArray()); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + + _saveItemCommand.GetParameter(index++).Value = item.CriticRating; + _saveItemCommand.GetParameter(index++).Value = item.CriticRatingSummary; + + var inheritedTags = item.GetInheritedTags(); + if (inheritedTags.Count > 0) + { + _saveItemCommand.GetParameter(index++).Value = string.Join("|", inheritedTags.ToArray()); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + + if (string.IsNullOrWhiteSpace(item.Name)) + { + _saveItemCommand.GetParameter(index++).Value = null; + } + else + { + _saveItemCommand.GetParameter(index++).Value = GetCleanValue(item.Name); + } + + _saveItemCommand.GetParameter(index++).Value = item.GetPresentationUniqueKey(); + _saveItemCommand.GetParameter(index++).Value = item.SlugName; + _saveItemCommand.GetParameter(index++).Value = item.OriginalTitle; + + var video = item as Video; + if (video != null) + { + _saveItemCommand.GetParameter(index++).Value = video.PrimaryVersionId; + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + + var folder = item as Folder; + if (folder != null && folder.DateLastMediaAdded.HasValue) + { + _saveItemCommand.GetParameter(index++).Value = folder.DateLastMediaAdded.Value; + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + + _saveItemCommand.GetParameter(index++).Value = item.Album; + + _saveItemCommand.GetParameter(index++).Value = item.IsVirtualItem; + + var hasSeries = item as IHasSeries; + if (hasSeries != null) + { + _saveItemCommand.GetParameter(index++).Value = hasSeries.FindSeriesName(); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + + _saveItemCommand.GetParameter(index++).Value = item.GetUserDataKeys().FirstOrDefault(); + + var episode = item as Episode; + if (episode != null) + { + _saveItemCommand.GetParameter(index++).Value = episode.FindSeasonName(); + _saveItemCommand.GetParameter(index++).Value = episode.FindSeasonId(); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + _saveItemCommand.GetParameter(index++).Value = null; + } + + if (hasSeries != null) + { + _saveItemCommand.GetParameter(index++).Value = hasSeries.FindSeriesId(); + _saveItemCommand.GetParameter(index++).Value = hasSeries.FindSeriesSortName(); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + _saveItemCommand.GetParameter(index++).Value = null; + } + + _saveItemCommand.GetParameter(index++).Value = item.ExternalSeriesId; + _saveItemCommand.GetParameter(index++).Value = item.ShortOverview; + _saveItemCommand.GetParameter(index++).Value = item.Tagline; + + if (item.Keywords.Count > 0) + { + _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.Keywords.ToArray()); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + + _saveItemCommand.GetParameter(index++).Value = SerializeProviderIds(item); + _saveItemCommand.GetParameter(index++).Value = SerializeImages(item); + + if (item.ProductionLocations.Count > 0) + { + _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.ProductionLocations.ToArray()); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + + if (item.ThemeSongIds.Count > 0) + { + _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.ThemeSongIds.Select(i => i.ToString("N")).ToArray()); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + + if (item.ThemeVideoIds.Count > 0) + { + _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.ThemeVideoIds.Select(i => i.ToString("N")).ToArray()); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + + _saveItemCommand.GetParameter(index++).Value = item.TotalBitrate; + _saveItemCommand.GetParameter(index++).Value = item.ExtraType; + + var hasArtists = item as IHasArtist; + if (hasArtists != null) + { + if (hasArtists.Artists.Count > 0) + { + _saveItemCommand.GetParameter(index++).Value = string.Join("|", hasArtists.Artists.ToArray()); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + } + + var hasAlbumArtists = item as IHasAlbumArtist; + if (hasAlbumArtists != null) + { + if (hasAlbumArtists.AlbumArtists.Count > 0) + { + _saveItemCommand.GetParameter(index++).Value = string.Join("|", hasAlbumArtists.AlbumArtists.ToArray()); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + } + + _saveItemCommand.Transaction = transaction; + + _saveItemCommand.ExecuteNonQuery(); + + if (item.SupportsAncestors) + { + UpdateAncestors(item.Id, item.GetAncestorIds().Distinct().ToList(), transaction); + } + + UpdateImages(item.Id, item.ImageInfos, transaction); + UpdateProviderIds(item.Id, item.ProviderIds, transaction); + UpdateItemValues(item.Id, GetItemValuesToSave(item), transaction); + } + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save items:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + + WriteLock.Release(); + } + } + + private string SerializeProviderIds(BaseItem item) + { + // Ideally we shouldn't need this IsNullOrWhiteSpace check but we're seeing some cases of bad data slip through + var ids = item.ProviderIds + .Where(i => !string.IsNullOrWhiteSpace(i.Value)) + .ToList(); + + if (ids.Count == 0) + { + return null; + } + + return string.Join("|", ids.Select(i => i.Key + "=" + i.Value).ToArray()); + } + + private void DeserializeProviderIds(string value, BaseItem item) + { + if (string.IsNullOrWhiteSpace(value)) + { + return; + } + + if (item.ProviderIds.Count > 0) + { + return; + } + + var parts = value.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + + foreach (var part in parts) + { + var idParts = part.Split('='); + + if (idParts.Length == 2) + { + item.SetProviderId(idParts[0], idParts[1]); + } + } + } + + private string SerializeImages(BaseItem item) + { + var images = item.ImageInfos.ToList(); + + if (images.Count == 0) + { + return null; + } + + return string.Join("|", images.Select(ToValueString).ToArray()); + } + + private void DeserializeImages(string value, BaseItem item) + { + if (string.IsNullOrWhiteSpace(value)) + { + return; + } + + if (item.ImageInfos.Count > 0) + { + return; + } + + var parts = value.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + + foreach (var part in parts) + { + item.ImageInfos.Add(ItemImageInfoFromValueString(part)); + } + } + + public string ToValueString(ItemImageInfo image) + { + var delimeter = "*"; + + return (image.Path ?? string.Empty) + + delimeter + + image.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + + delimeter + + image.Type + + delimeter + + image.IsPlaceholder; + } + + public ItemImageInfo ItemImageInfoFromValueString(string value) + { + var parts = value.Split(new[] { '*' }, StringSplitOptions.None); + + var image = new ItemImageInfo(); + + image.Path = parts[0]; + image.DateModified = new DateTime(long.Parse(parts[1], CultureInfo.InvariantCulture), DateTimeKind.Utc); + image.Type = (ImageType)Enum.Parse(typeof(ImageType), parts[2], true); + image.IsPlaceholder = string.Equals(parts[3], true.ToString(), StringComparison.OrdinalIgnoreCase); + + return image; + } + + /// + /// Internal retrieve from items or users table + /// + /// The id. + /// BaseItem. + /// id + /// + public BaseItem RetrieveItem(Guid id) + { + if (id == Guid.Empty) + { + throw new ArgumentNullException("id"); + } + + CheckDisposed(); + + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "select " + string.Join(",", _retriveItemColumns) + " from TypedBaseItems where guid = @guid"; + cmd.Parameters.Add(cmd, "@guid", DbType.Guid).Value = id; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) + { + if (reader.Read()) + { + return GetItem(reader); + } + } + return null; + } + } + + private BaseItem GetItem(IDataReader reader) + { + return GetItem(reader, new InternalItemsQuery()); + } + + private bool TypeRequiresDeserialization(Type type) + { + if (_config.Configuration.SkipDeserializationForBasicTypes) + { + if (type == typeof(MusicGenre)) + { + return false; + } + if (type == typeof(GameGenre)) + { + return false; + } + if (type == typeof(Genre)) + { + return false; + } + if (type == typeof(Studio)) + { + return false; + } + if (type == typeof(Year)) + { + return false; + } + if (type == typeof(Book)) + { + return false; + } + if (type == typeof(Person)) + { + return false; + } + if (type == typeof(RecordingGroup)) + { + return false; + } + if (type == typeof(Channel)) + { + return false; + } + if (type == typeof(ManualCollectionsFolder)) + { + return false; + } + if (type == typeof(CameraUploadsFolder)) + { + return false; + } + if (type == typeof(PlaylistsFolder)) + { + return false; + } + if (type == typeof(UserRootFolder)) + { + return false; + } + if (type == typeof(PhotoAlbum)) + { + return false; + } + if (type == typeof(Season)) + { + return false; + } + if (type == typeof(MusicArtist)) + { + return false; + } + } + if (_config.Configuration.SkipDeserializationForPrograms) + { + if (type == typeof(LiveTvProgram)) + { + return false; + } + } + if (_config.Configuration.SkipDeserializationForAudio) + { + if (type == typeof(Audio)) + { + return false; + } + if (type == typeof(LiveTvAudioRecording)) + { + return false; + } + if (type == typeof(AudioPodcast)) + { + return false; + } + if (type == typeof(MusicAlbum)) + { + return false; + } + } + + return true; + } + + private BaseItem GetItem(IDataReader reader, InternalItemsQuery query) + { + var typeString = reader.GetString(0); + + var type = _typeMapper.GetType(typeString); + + if (type == null) + { + //Logger.Debug("Unknown type {0}", typeString); + + return null; + } + + BaseItem item = null; + + if (TypeRequiresDeserialization(type)) + { + using (var stream = reader.GetMemoryStream(1, _memoryStreamProvider)) + { + try + { + item = _jsonSerializer.DeserializeFromStream(stream, type) as BaseItem; + } + catch (SerializationException ex) + { + Logger.ErrorException("Error deserializing item", ex); + } + } + } + + if (item == null) + { + try + { + item = Activator.CreateInstance(type) as BaseItem; + } + catch + { + } + } + + if (item == null) + { + return null; + } + + if (!reader.IsDBNull(2)) + { + var hasStartDate = item as IHasStartDate; + if (hasStartDate != null) + { + hasStartDate.StartDate = reader.GetDateTime(2).ToUniversalTime(); + } + } + + if (!reader.IsDBNull(3)) + { + item.EndDate = reader.GetDateTime(3).ToUniversalTime(); + } + + if (!reader.IsDBNull(4)) + { + item.IsOffline = reader.GetBoolean(4); + } + + if (!reader.IsDBNull(5)) + { + item.ChannelId = reader.GetString(5); + } + + var hasProgramAttributes = item as IHasProgramAttributes; + if (hasProgramAttributes != null) + { + if (!reader.IsDBNull(6)) + { + hasProgramAttributes.IsMovie = reader.GetBoolean(6); + } + + if (!reader.IsDBNull(7)) + { + hasProgramAttributes.IsSports = reader.GetBoolean(7); + } + + if (!reader.IsDBNull(8)) + { + hasProgramAttributes.IsKids = reader.GetBoolean(8); + } + + if (!reader.IsDBNull(9)) + { + hasProgramAttributes.IsSeries = reader.GetBoolean(9); + } + + if (!reader.IsDBNull(10)) + { + hasProgramAttributes.IsLive = reader.GetBoolean(10); + } + + if (!reader.IsDBNull(11)) + { + hasProgramAttributes.IsNews = reader.GetBoolean(11); + } + + if (!reader.IsDBNull(12)) + { + hasProgramAttributes.IsPremiere = reader.GetBoolean(12); + } + + if (!reader.IsDBNull(13)) + { + hasProgramAttributes.EpisodeTitle = reader.GetString(13); + } + + if (!reader.IsDBNull(14)) + { + hasProgramAttributes.IsRepeat = reader.GetBoolean(14); + } + } + + var index = 15; + + if (!reader.IsDBNull(index)) + { + item.CommunityRating = reader.GetFloat(index); + } + index++; + + if (query.HasField(ItemFields.CustomRating)) + { + if (!reader.IsDBNull(index)) + { + item.CustomRating = reader.GetString(index); + } + index++; + } + + if (!reader.IsDBNull(index)) + { + item.IndexNumber = reader.GetInt32(index); + } + index++; + + if (query.HasField(ItemFields.Settings)) + { + if (!reader.IsDBNull(index)) + { + item.IsLocked = reader.GetBoolean(index); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.PreferredMetadataLanguage = reader.GetString(index); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.PreferredMetadataCountryCode = reader.GetString(index); + } + index++; + } + + if (!reader.IsDBNull(index)) + { + item.IsHD = reader.GetBoolean(index); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.ExternalEtag = reader.GetString(index); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.DateLastRefreshed = reader.GetDateTime(index).ToUniversalTime(); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.Name = reader.GetString(index); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.Path = reader.GetString(index); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.PremiereDate = reader.GetDateTime(index).ToUniversalTime(); + } + index++; + + if (query.HasField(ItemFields.Overview)) + { + if (!reader.IsDBNull(index)) + { + item.Overview = reader.GetString(index); + } + index++; + } + + if (!reader.IsDBNull(index)) + { + item.ParentIndexNumber = reader.GetInt32(index); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.ProductionYear = reader.GetInt32(index); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.OfficialRating = reader.GetString(index); + } + index++; + + if (query.HasField(ItemFields.OfficialRatingDescription)) + { + if (!reader.IsDBNull(index)) + { + item.OfficialRatingDescription = reader.GetString(index); + } + index++; + } + + if (query.HasField(ItemFields.HomePageUrl)) + { + if (!reader.IsDBNull(index)) + { + item.HomePageUrl = reader.GetString(index); + } + index++; + } + + if (query.HasField(ItemFields.DisplayMediaType)) + { + if (!reader.IsDBNull(index)) + { + item.DisplayMediaType = reader.GetString(index); + } + index++; + } + + if (query.HasField(ItemFields.SortName)) + { + if (!reader.IsDBNull(index)) + { + item.ForcedSortName = reader.GetString(index); + } + index++; + } + + if (!reader.IsDBNull(index)) + { + item.RunTimeTicks = reader.GetInt64(index); + } + index++; + + if (query.HasField(ItemFields.VoteCount)) + { + if (!reader.IsDBNull(index)) + { + item.VoteCount = reader.GetInt32(index); + } + index++; + } + + if (query.HasField(ItemFields.DateCreated)) + { + if (!reader.IsDBNull(index)) + { + item.DateCreated = reader.GetDateTime(index).ToUniversalTime(); + } + index++; + } + + if (!reader.IsDBNull(index)) + { + item.DateModified = reader.GetDateTime(index).ToUniversalTime(); + } + index++; + + item.Id = reader.GetGuid(index); + index++; + + if (query.HasField(ItemFields.Genres)) + { + if (!reader.IsDBNull(index)) + { + item.Genres = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + } + index++; + } + + if (!reader.IsDBNull(index)) + { + item.ParentId = reader.GetGuid(index); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.Audio = (ProgramAudio)Enum.Parse(typeof(ProgramAudio), reader.GetString(index), true); + } + index++; + + // TODO: Even if not needed by apps, the server needs it internally + // But get this excluded from contexts where it is not needed + if (!reader.IsDBNull(index)) + { + item.ServiceName = reader.GetString(index); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.IsInMixedFolder = reader.GetBoolean(index); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.DateLastSaved = reader.GetDateTime(index).ToUniversalTime(); + } + index++; + + if (query.HasField(ItemFields.Settings)) + { + if (!reader.IsDBNull(index)) + { + item.LockedFields = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => (MetadataFields)Enum.Parse(typeof(MetadataFields), i, true)).ToList(); + } + index++; + } + + if (query.HasField(ItemFields.Studios)) + { + if (!reader.IsDBNull(index)) + { + item.Studios = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + } + index++; + } + + if (query.HasField(ItemFields.Tags)) + { + if (!reader.IsDBNull(index)) + { + item.Tags = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + } + index++; + } + + if (!reader.IsDBNull(index)) + { + item.SourceType = (SourceType)Enum.Parse(typeof(SourceType), reader.GetString(index), true); + } + index++; + + var trailer = item as Trailer; + if (trailer != null) + { + if (!reader.IsDBNull(index)) + { + trailer.TrailerTypes = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => (TrailerType)Enum.Parse(typeof(TrailerType), i, true)).ToList(); + } + } + index++; + + if (query.HasField(ItemFields.OriginalTitle)) + { + if (!reader.IsDBNull(index)) + { + item.OriginalTitle = reader.GetString(index); + } + index++; + } + + var video = item as Video; + if (video != null) + { + if (!reader.IsDBNull(index)) + { + video.PrimaryVersionId = reader.GetString(index); + } + } + index++; + + if (query.HasField(ItemFields.DateLastMediaAdded)) + { + var folder = item as Folder; + if (folder != null && !reader.IsDBNull(index)) + { + folder.DateLastMediaAdded = reader.GetDateTime(index).ToUniversalTime(); + } + index++; + } + + if (!reader.IsDBNull(index)) + { + item.Album = reader.GetString(index); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.CriticRating = reader.GetFloat(index); + } + index++; + + if (query.HasField(ItemFields.CriticRatingSummary)) + { + if (!reader.IsDBNull(index)) + { + item.CriticRatingSummary = reader.GetString(index); + } + index++; + } + + if (!reader.IsDBNull(index)) + { + item.IsVirtualItem = reader.GetBoolean(index); + } + index++; + + var hasSeries = item as IHasSeries; + if (hasSeries != null) + { + if (!reader.IsDBNull(index)) + { + hasSeries.SeriesName = reader.GetString(index); + } + } + index++; + + var episode = item as Episode; + if (episode != null) + { + if (!reader.IsDBNull(index)) + { + episode.SeasonName = reader.GetString(index); + } + index++; + if (!reader.IsDBNull(index)) + { + episode.SeasonId = reader.GetGuid(index); + } + } + else + { + index++; + } + index++; + + if (hasSeries != null) + { + if (!reader.IsDBNull(index)) + { + hasSeries.SeriesId = reader.GetGuid(index); + } + } + index++; + + if (hasSeries != null) + { + if (!reader.IsDBNull(index)) + { + hasSeries.SeriesSortName = reader.GetString(index); + } + } + index++; + + if (!reader.IsDBNull(index)) + { + item.PresentationUniqueKey = reader.GetString(index); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.InheritedParentalRatingValue = reader.GetInt32(index); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.InheritedTags = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.ExternalSeriesId = reader.GetString(index); + } + index++; + + if (query.HasField(ItemFields.ShortOverview)) + { + if (!reader.IsDBNull(index)) + { + item.ShortOverview = reader.GetString(index); + } + index++; + } + + if (query.HasField(ItemFields.Taglines)) + { + if (!reader.IsDBNull(index)) + { + item.Tagline = reader.GetString(index); + } + index++; + } + + if (query.HasField(ItemFields.Keywords)) + { + if (!reader.IsDBNull(index)) + { + item.Keywords = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + } + index++; + } + + if (!reader.IsDBNull(index)) + { + DeserializeProviderIds(reader.GetString(index), item); + } + index++; + + if (query.DtoOptions.EnableImages) + { + if (!reader.IsDBNull(index)) + { + DeserializeImages(reader.GetString(index), item); + } + index++; + } + + if (query.HasField(ItemFields.ProductionLocations)) + { + if (!reader.IsDBNull(index)) + { + item.ProductionLocations = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + } + index++; + } + + if (query.HasField(ItemFields.ThemeSongIds)) + { + if (!reader.IsDBNull(index)) + { + item.ThemeSongIds = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => new Guid(i)).ToList(); + } + index++; + } + + if (query.HasField(ItemFields.ThemeVideoIds)) + { + if (!reader.IsDBNull(index)) + { + item.ThemeVideoIds = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => new Guid(i)).ToList(); + } + index++; + } + + if (!reader.IsDBNull(index)) + { + item.TotalBitrate = reader.GetInt32(index); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.ExtraType = (ExtraType)Enum.Parse(typeof(ExtraType), reader.GetString(index), true); + } + index++; + + var hasArtists = item as IHasArtist; + if (hasArtists != null && !reader.IsDBNull(index)) + { + hasArtists.Artists = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + } + index++; + + var hasAlbumArtists = item as IHasAlbumArtist; + if (hasAlbumArtists != null && !reader.IsDBNull(index)) + { + hasAlbumArtists.AlbumArtists = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + } + index++; + + if (string.IsNullOrWhiteSpace(item.Tagline)) + { + var movie = item as Movie; + if (movie != null && movie.Taglines.Count > 0) + { + movie.Tagline = movie.Taglines[0]; + } + } + + if (type == typeof(Person) && item.ProductionLocations.Count == 0) + { + var person = (Person)item; + if (!string.IsNullOrWhiteSpace(person.PlaceOfBirth)) + { + item.ProductionLocations = new List { person.PlaceOfBirth }; + } + } + + return item; + } + + /// + /// Gets the critic reviews. + /// + /// The item id. + /// Task{IEnumerable{ItemReview}}. + public IEnumerable GetCriticReviews(Guid itemId) + { + try + { + var path = Path.Combine(_criticReviewsPath, itemId + ".json"); + + return _jsonSerializer.DeserializeFromFile>(path); + } + catch (DirectoryNotFoundException) + { + return new List(); + } + catch (FileNotFoundException) + { + return new List(); + } + } + + private readonly Task _cachedTask = Task.FromResult(true); + /// + /// Saves the critic reviews. + /// + /// The item id. + /// The critic reviews. + /// Task. + public Task SaveCriticReviews(Guid itemId, IEnumerable criticReviews) + { + Directory.CreateDirectory(_criticReviewsPath); + + var path = Path.Combine(_criticReviewsPath, itemId + ".json"); + + _jsonSerializer.SerializeToFile(criticReviews.ToList(), path); + + return _cachedTask; + } + + /// + /// Gets chapters for an item + /// + /// The id. + /// IEnumerable{ChapterInfo}. + /// id + public IEnumerable GetChapters(Guid id) + { + CheckDisposed(); + if (id == Guid.Empty) + { + throw new ArgumentNullException("id"); + } + var list = new List(); + + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"; + + cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = id; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + list.Add(GetChapter(reader)); + } + } + } + + return list; + } + + /// + /// Gets a single chapter for an item + /// + /// The id. + /// The index. + /// ChapterInfo. + /// id + public ChapterInfo GetChapter(Guid id, int index) + { + CheckDisposed(); + if (id == Guid.Empty) + { + throw new ArgumentNullException("id"); + } + + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"; + + cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = id; + cmd.Parameters.Add(cmd, "@ChapterIndex", DbType.Int32).Value = index; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) + { + if (reader.Read()) + { + return GetChapter(reader); + } + } + return null; + } + } + + /// + /// Gets the chapter. + /// + /// The reader. + /// ChapterInfo. + private ChapterInfo GetChapter(IDataReader reader) + { + var chapter = new ChapterInfo + { + StartPositionTicks = reader.GetInt64(0) + }; + + if (!reader.IsDBNull(1)) + { + chapter.Name = reader.GetString(1); + } + + if (!reader.IsDBNull(2)) + { + chapter.ImagePath = reader.GetString(2); + } + + if (!reader.IsDBNull(3)) + { + chapter.ImageDateModified = reader.GetDateTime(3).ToUniversalTime(); + } + + return chapter; + } + + /// + /// Saves the chapters. + /// + /// The id. + /// The chapters. + /// The cancellation token. + /// Task. + /// + /// id + /// or + /// chapters + /// or + /// cancellationToken + /// + public async Task SaveChapters(Guid id, List chapters, CancellationToken cancellationToken) + { + CheckDisposed(); + + if (id == Guid.Empty) + { + throw new ArgumentNullException("id"); + } + + if (chapters == null) + { + throw new ArgumentNullException("chapters"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + IDbTransaction transaction = null; + + try + { + transaction = _connection.BeginTransaction(); + + // First delete chapters + _deleteChaptersCommand.GetParameter(0).Value = id; + + _deleteChaptersCommand.Transaction = transaction; + + _deleteChaptersCommand.ExecuteNonQuery(); + + var index = 0; + + foreach (var chapter in chapters) + { + cancellationToken.ThrowIfCancellationRequested(); + + _saveChapterCommand.GetParameter(0).Value = id; + _saveChapterCommand.GetParameter(1).Value = index; + _saveChapterCommand.GetParameter(2).Value = chapter.StartPositionTicks; + _saveChapterCommand.GetParameter(3).Value = chapter.Name; + _saveChapterCommand.GetParameter(4).Value = chapter.ImagePath; + _saveChapterCommand.GetParameter(5).Value = chapter.ImageDateModified; + + _saveChapterCommand.Transaction = transaction; + + _saveChapterCommand.ExecuteNonQuery(); + + index++; + } + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save chapters:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + + WriteLock.Release(); + } + } + + protected override void CloseConnection() + { + if (_connection != null) + { + if (_connection.IsOpen()) + { + _connection.Close(); + } + + _connection.Dispose(); + _connection = null; + } + } + + private bool EnableJoinUserData(InternalItemsQuery query) + { + if (query.User == null) + { + return false; + } + + if (query.SimilarTo != null && query.User != null) + { + return true; + } + + var sortingFields = query.SortBy.ToList(); + sortingFields.AddRange(query.OrderBy.Select(i => i.Item1)); + + if (sortingFields.Contains(ItemSortBy.IsFavoriteOrLiked, StringComparer.OrdinalIgnoreCase)) + { + return true; + } + if (sortingFields.Contains(ItemSortBy.IsPlayed, StringComparer.OrdinalIgnoreCase)) + { + return true; + } + if (sortingFields.Contains(ItemSortBy.IsUnplayed, StringComparer.OrdinalIgnoreCase)) + { + return true; + } + if (sortingFields.Contains(ItemSortBy.PlayCount, StringComparer.OrdinalIgnoreCase)) + { + return true; + } + if (sortingFields.Contains(ItemSortBy.DatePlayed, StringComparer.OrdinalIgnoreCase)) + { + return true; + } + + if (query.IsFavoriteOrLiked.HasValue) + { + return true; + } + + if (query.IsFavorite.HasValue) + { + return true; + } + + if (query.IsResumable.HasValue) + { + return true; + } + + if (query.IsPlayed.HasValue) + { + return true; + } + + if (query.IsLiked.HasValue) + { + return true; + } + + return false; + } + + private List allFields = Enum.GetNames(typeof(ItemFields)) + .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)) + .ToList(); + + private IEnumerable GetColumnNamesFromField(ItemFields field) + { + if (field == ItemFields.Settings) + { + return new[] { "IsLocked", "PreferredMetadataCountryCode", "PreferredMetadataLanguage", "LockedFields" }; + } + if (field == ItemFields.ServiceName) + { + return new[] { "ExternalServiceId" }; + } + if (field == ItemFields.SortName) + { + return new[] { "ForcedSortName" }; + } + if (field == ItemFields.Taglines) + { + return new[] { "Tagline" }; + } + + return new[] { field.ToString() }; + } + + private string[] GetFinalColumnsToSelect(InternalItemsQuery query, string[] startColumns, IDbCommand cmd) + { + var list = startColumns.ToList(); + + foreach (var field in allFields) + { + if (!query.HasField(field)) + { + foreach (var fieldToRemove in GetColumnNamesFromField(field).ToList()) + { + list.Remove(fieldToRemove); + } + } + } + + if (!query.DtoOptions.EnableImages) + { + list.Remove("Images"); + } + + if (EnableJoinUserData(query)) + { + list.Add("UserDataDb.UserData.UserId"); + list.Add("UserDataDb.UserData.lastPlayedDate"); + list.Add("UserDataDb.UserData.playbackPositionTicks"); + list.Add("UserDataDb.UserData.playcount"); + list.Add("UserDataDb.UserData.isFavorite"); + list.Add("UserDataDb.UserData.played"); + list.Add("UserDataDb.UserData.rating"); + } + + if (query.SimilarTo != null) + { + var item = query.SimilarTo; + + var builder = new StringBuilder(); + builder.Append("("); + + builder.Append("((OfficialRating=@ItemOfficialRating) * 10)"); + //builder.Append("+ ((ProductionYear=@ItemProductionYear) * 10)"); + + builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 10 Then 2 Else 0 End )"); + builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 5 Then 2 Else 0 End )"); + + //// genres + builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and Type=2 and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId and type=2)) * 10)"); + + //// tags + builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and Type=4 and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId and type=4)) * 10)"); + + builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and Type=5 and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId and type=5)) * 10)"); + + builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and Type=3 and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId and type=3)) * 3)"); + + //builder.Append("+ ((Select count(Name) from People where ItemId=Guid and Name in (select Name from People where ItemId=@SimilarItemId)) * 3)"); + + ////builder.Append("(select group_concat((Select Name from People where ItemId=Guid and Name in (Select Name from People where ItemId=@SimilarItemId)), '|'))"); + + builder.Append(") as SimilarityScore"); + + list.Add(builder.ToString()); + cmd.Parameters.Add(cmd, "@ItemOfficialRating", DbType.String).Value = item.OfficialRating; + cmd.Parameters.Add(cmd, "@ItemProductionYear", DbType.Int32).Value = item.ProductionYear ?? 0; + cmd.Parameters.Add(cmd, "@SimilarItemId", DbType.Guid).Value = item.Id; + + var excludeIds = query.ExcludeItemIds.ToList(); + excludeIds.Add(item.Id.ToString("N")); + query.ExcludeItemIds = excludeIds.ToArray(); + + query.ExcludeProviderIds = item.ProviderIds; + } + + return list.ToArray(); + } + + private string GetJoinUserDataText(InternalItemsQuery query) + { + if (!EnableJoinUserData(query)) + { + return string.Empty; + } + + return " left join UserDataDb.UserData on UserDataKey=UserDataDb.UserData.Key And (UserId=@UserId)"; + } + + private string GetGroupBy(InternalItemsQuery query) + { + var groups = new List(); + + if (EnableGroupByPresentationUniqueKey(query)) + { + groups.Add("PresentationUniqueKey"); + } + + if (groups.Count > 0) + { + return " Group by " + string.Join(",", groups.ToArray()); + } + + return string.Empty; + } + + private string GetFromText(string alias = "A") + { + return " from TypedBaseItems " + alias; + } + + public List GetItemList(InternalItemsQuery query) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + CheckDisposed(); + + var now = DateTime.UtcNow; + + var list = new List(); + + // Hack for right now since we currently don't support filtering out these duplicates within a query + if (query.Limit.HasValue && query.EnableGroupByMetadataKey) + { + query.Limit = query.Limit.Value + 4; + } + + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, _retriveItemColumns, cmd)) + GetFromText(); + cmd.CommandText += GetJoinUserDataText(query); + + if (EnableJoinUserData(query)) + { + cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = query.User.Id; + } + + var whereClauses = GetWhereClauses(query, cmd); + + var whereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + cmd.CommandText += whereText; + + cmd.CommandText += GetGroupBy(query); + + cmd.CommandText += GetOrderByText(query); + + if (query.Limit.HasValue || query.StartIndex.HasValue) + { + var offset = query.StartIndex ?? 0; + + if (query.Limit.HasValue || offset > 0) + { + cmd.CommandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); + } + + if (offset > 0) + { + cmd.CommandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); + } + } + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + LogQueryTime("GetItemList", cmd, now); + + while (reader.Read()) + { + var item = GetItem(reader, query); + if (item != null) + { + list.Add(item); + } + } + } + } + + // Hack for right now since we currently don't support filtering out these duplicates within a query + if (query.EnableGroupByMetadataKey) + { + var limit = query.Limit ?? int.MaxValue; + limit -= 4; + var newList = new List(); + + foreach (var item in list) + { + AddItem(newList, item); + + if (newList.Count >= limit) + { + break; + } + } + + list = newList; + } + + return list; + } + + private void AddItem(List items, BaseItem newItem) + { + var providerIds = newItem.ProviderIds.ToList(); + + for (var i = 0; i < items.Count; i++) + { + var item = items[i]; + + foreach (var providerId in providerIds) + { + if (providerId.Key == MetadataProviders.TmdbCollection.ToString()) + { + continue; + } + if (item.GetProviderId(providerId.Key) == providerId.Value) + { + if (newItem.SourceType == SourceType.Library) + { + items[i] = newItem; + } + return; + } + } + } + + items.Add(newItem); + } + + private void LogQueryTime(string methodName, IDbCommand cmd, DateTime startDate) + { + var elapsed = (DateTime.UtcNow - startDate).TotalMilliseconds; + + var slowThreshold = 1000; + +#if DEBUG + slowThreshold = 50; +#endif + + if (elapsed >= slowThreshold) + { + Logger.Debug("{2} query time (slow): {0}ms. Query: {1}", + Convert.ToInt32(elapsed), + cmd.CommandText, + methodName); + } + else + { + //Logger.Debug("{2} query time: {0}ms. Query: {1}", + // Convert.ToInt32(elapsed), + // cmd.CommandText, + // methodName); + } + } + + public QueryResult GetItems(InternalItemsQuery query) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + CheckDisposed(); + + if (!query.EnableTotalRecordCount || (!query.Limit.HasValue && (query.StartIndex ?? 0) == 0)) + { + var list = GetItemList(query); + return new QueryResult + { + Items = list.ToArray(), + TotalRecordCount = list.Count + }; + } + + var now = DateTime.UtcNow; + + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, _retriveItemColumns, cmd)) + GetFromText(); + cmd.CommandText += GetJoinUserDataText(query); + + if (EnableJoinUserData(query)) + { + cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = query.User.Id; + } + + var whereClauses = GetWhereClauses(query, cmd); + + var whereTextWithoutPaging = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + var whereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + cmd.CommandText += whereText; + + cmd.CommandText += GetGroupBy(query); + + cmd.CommandText += GetOrderByText(query); + + if (query.Limit.HasValue || query.StartIndex.HasValue) + { + var offset = query.StartIndex ?? 0; + + if (query.Limit.HasValue || offset > 0) + { + cmd.CommandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); + } + + if (offset > 0) + { + cmd.CommandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); + } + } + + cmd.CommandText += ";"; + + var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; + + if (isReturningZeroItems) + { + cmd.CommandText = ""; + } + + if (EnableGroupByPresentationUniqueKey(query)) + { + cmd.CommandText += " select count (distinct PresentationUniqueKey)" + GetFromText(); + } + else + { + cmd.CommandText += " select count (guid)" + GetFromText(); + } + + cmd.CommandText += GetJoinUserDataText(query); + cmd.CommandText += whereTextWithoutPaging; + + var list = new List(); + var count = 0; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) + { + LogQueryTime("GetItems", cmd, now); + + if (isReturningZeroItems) + { + if (reader.Read()) + { + count = reader.GetInt32(0); + } + } + else + { + while (reader.Read()) + { + var item = GetItem(reader, query); + if (item != null) + { + list.Add(item); + } + } + + if (reader.NextResult() && reader.Read()) + { + count = reader.GetInt32(0); + } + } + } + + return new QueryResult() + { + Items = list.ToArray(), + TotalRecordCount = count + }; + } + } + + private string GetOrderByText(InternalItemsQuery query) + { + var orderBy = query.OrderBy.ToList(); + var enableOrderInversion = true; + + if (orderBy.Count == 0) + { + orderBy.AddRange(query.SortBy.Select(i => new Tuple(i, query.SortOrder))); + } + else + { + enableOrderInversion = false; + } + + if (query.SimilarTo != null) + { + if (orderBy.Count == 0) + { + orderBy.Add(new Tuple("SimilarityScore", SortOrder.Descending)); + orderBy.Add(new Tuple(ItemSortBy.Random, SortOrder.Ascending)); + query.SortOrder = SortOrder.Descending; + enableOrderInversion = false; + } + } + + query.OrderBy = orderBy; + + if (orderBy.Count == 0) + { + return string.Empty; + } + + return " ORDER BY " + string.Join(",", orderBy.Select(i => + { + var columnMap = MapOrderByField(i.Item1, query); + var columnAscending = i.Item2 == SortOrder.Ascending; + if (columnMap.Item2 && enableOrderInversion) + { + columnAscending = !columnAscending; + } + + var sortOrder = columnAscending ? "ASC" : "DESC"; + + return columnMap.Item1 + " " + sortOrder; + }).ToArray()); + } + + private Tuple MapOrderByField(string name, InternalItemsQuery query) + { + if (string.Equals(name, ItemSortBy.AirTime, StringComparison.OrdinalIgnoreCase)) + { + // TODO + return new Tuple("SortName", false); + } + if (string.Equals(name, ItemSortBy.Runtime, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple("RuntimeTicks", false); + } + if (string.Equals(name, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple("RANDOM()", false); + } + if (string.Equals(name, ItemSortBy.DatePlayed, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple("LastPlayedDate", false); + } + if (string.Equals(name, ItemSortBy.PlayCount, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple("PlayCount", false); + } + if (string.Equals(name, ItemSortBy.IsFavoriteOrLiked, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple("IsFavorite", true); + } + if (string.Equals(name, ItemSortBy.IsFolder, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple("IsFolder", true); + } + if (string.Equals(name, ItemSortBy.IsPlayed, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple("played", true); + } + if (string.Equals(name, ItemSortBy.IsUnplayed, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple("played", false); + } + if (string.Equals(name, ItemSortBy.DateLastContentAdded, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple("DateLastMediaAdded", false); + } + if (string.Equals(name, ItemSortBy.Artist, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple("(select CleanValue from itemvalues where ItemId=Guid and Type=0 LIMIT 1)", false); + } + if (string.Equals(name, ItemSortBy.AlbumArtist, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple("(select CleanValue from itemvalues where ItemId=Guid and Type=1 LIMIT 1)", false); + } + if (string.Equals(name, ItemSortBy.OfficialRating, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple("InheritedParentalRatingValue", false); + } + if (string.Equals(name, ItemSortBy.Studio, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple("(select CleanValue from itemvalues where ItemId=Guid and Type=3 LIMIT 1)", false); + } + if (string.Equals(name, ItemSortBy.SeriesDatePlayed, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple("(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where B.Guid in (Select ItemId from AncestorIds where AncestorId in (select guid from typedbaseitems c where C.Type = 'MediaBrowser.Controller.Entities.TV.Series' And C.Guid in (Select AncestorId from AncestorIds where ItemId=A.Guid))))", false); + } + + return new Tuple(name, false); + } + + public List GetItemIdsList(InternalItemsQuery query) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + CheckDisposed(); + + var now = DateTime.UtcNow; + + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid" }, cmd)) + GetFromText(); + cmd.CommandText += GetJoinUserDataText(query); + + if (EnableJoinUserData(query)) + { + cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = query.User.Id; + } + + var whereClauses = GetWhereClauses(query, cmd); + + var whereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + cmd.CommandText += whereText; + + cmd.CommandText += GetGroupBy(query); + + cmd.CommandText += GetOrderByText(query); + + if (query.Limit.HasValue || query.StartIndex.HasValue) + { + var offset = query.StartIndex ?? 0; + + if (query.Limit.HasValue || offset > 0) + { + cmd.CommandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); + } + + if (offset > 0) + { + cmd.CommandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); + } + } + + var list = new List(); + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + LogQueryTime("GetItemIdsList", cmd, now); + + while (reader.Read()) + { + list.Add(reader.GetGuid(0)); + } + } + + return list; + } + } + + public QueryResult> GetItemIdsWithPath(InternalItemsQuery query) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + CheckDisposed(); + + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "select guid,path from TypedBaseItems"; + + var whereClauses = GetWhereClauses(query, cmd); + + var whereTextWithoutPaging = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + var whereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + cmd.CommandText += whereText; + + cmd.CommandText += GetGroupBy(query); + + cmd.CommandText += GetOrderByText(query); + + if (query.Limit.HasValue || query.StartIndex.HasValue) + { + var offset = query.StartIndex ?? 0; + + if (query.Limit.HasValue || offset > 0) + { + cmd.CommandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); + } + + if (offset > 0) + { + cmd.CommandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); + } + } + + cmd.CommandText += "; select count (guid) from TypedBaseItems" + whereTextWithoutPaging; + + var list = new List>(); + var count = 0; + + Logger.Debug(cmd.CommandText); + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) + { + while (reader.Read()) + { + var id = reader.GetGuid(0); + string path = null; + + if (!reader.IsDBNull(1)) + { + path = reader.GetString(1); + } + list.Add(new Tuple(id, path)); + } + + if (reader.NextResult() && reader.Read()) + { + count = reader.GetInt32(0); + } + } + + return new QueryResult>() + { + Items = list.ToArray(), + TotalRecordCount = count + }; + } + } + + public QueryResult GetItemIds(InternalItemsQuery query) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + CheckDisposed(); + + if (!query.EnableTotalRecordCount || (!query.Limit.HasValue && (query.StartIndex ?? 0) == 0)) + { + var list = GetItemIdsList(query); + return new QueryResult + { + Items = list.ToArray(), + TotalRecordCount = list.Count + }; + } + + var now = DateTime.UtcNow; + + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid" }, cmd)) + GetFromText(); + + var whereClauses = GetWhereClauses(query, cmd); + cmd.CommandText += GetJoinUserDataText(query); + + if (EnableJoinUserData(query)) + { + cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = query.User.Id; + } + + var whereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + cmd.CommandText += whereText; + + cmd.CommandText += GetGroupBy(query); + + cmd.CommandText += GetOrderByText(query); + + if (query.Limit.HasValue || query.StartIndex.HasValue) + { + var offset = query.StartIndex ?? 0; + + if (query.Limit.HasValue || offset > 0) + { + cmd.CommandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); + } + + if (offset > 0) + { + cmd.CommandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); + } + } + + if (EnableGroupByPresentationUniqueKey(query)) + { + cmd.CommandText += "; select count (distinct PresentationUniqueKey)" + GetFromText(); + } + else + { + cmd.CommandText += "; select count (guid)" + GetFromText(); + } + + cmd.CommandText += GetJoinUserDataText(query); + cmd.CommandText += whereText; + + var list = new List(); + var count = 0; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) + { + LogQueryTime("GetItemIds", cmd, now); + + while (reader.Read()) + { + list.Add(reader.GetGuid(0)); + } + + if (reader.NextResult() && reader.Read()) + { + count = reader.GetInt32(0); + } + } + + return new QueryResult() + { + Items = list.ToArray(), + TotalRecordCount = count + }; + } + } + + private List GetWhereClauses(InternalItemsQuery query, IDbCommand cmd, string paramSuffix = "") + { + var whereClauses = new List(); + + if (EnableJoinUserData(query)) + { + //whereClauses.Add("(UserId is null or UserId=@UserId)"); + } + if (query.IsCurrentSchema.HasValue) + { + if (query.IsCurrentSchema.Value) + { + whereClauses.Add("(SchemaVersion not null AND SchemaVersion=@SchemaVersion)"); + } + else + { + whereClauses.Add("(SchemaVersion is null or SchemaVersion<>@SchemaVersion)"); + } + cmd.Parameters.Add(cmd, "@SchemaVersion", DbType.Int32).Value = LatestSchemaVersion; + } + if (query.IsHD.HasValue) + { + whereClauses.Add("IsHD=@IsHD"); + cmd.Parameters.Add(cmd, "@IsHD", DbType.Boolean).Value = query.IsHD; + } + if (query.IsLocked.HasValue) + { + whereClauses.Add("IsLocked=@IsLocked"); + cmd.Parameters.Add(cmd, "@IsLocked", DbType.Boolean).Value = query.IsLocked; + } + if (query.IsOffline.HasValue) + { + whereClauses.Add("IsOffline=@IsOffline"); + cmd.Parameters.Add(cmd, "@IsOffline", DbType.Boolean).Value = query.IsOffline; + } + + var exclusiveProgramAttribtues = !(query.IsMovie ?? true) || + !(query.IsSports ?? true) || + !(query.IsKids ?? true) || + !(query.IsNews ?? true) || + !(query.IsSeries ?? true); + + if (exclusiveProgramAttribtues) + { + if (query.IsMovie.HasValue) + { + var alternateTypes = new List(); + if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Movie).Name)) + { + alternateTypes.Add(typeof(Movie).FullName); + } + if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Trailer).Name)) + { + alternateTypes.Add(typeof(Trailer).FullName); + } + + if (alternateTypes.Count == 0) + { + whereClauses.Add("IsMovie=@IsMovie"); + cmd.Parameters.Add(cmd, "@IsMovie", DbType.Boolean).Value = query.IsMovie; + } + else + { + whereClauses.Add("(IsMovie is null OR IsMovie=@IsMovie)"); + cmd.Parameters.Add(cmd, "@IsMovie", DbType.Boolean).Value = query.IsMovie; + } + } + if (query.IsSeries.HasValue) + { + whereClauses.Add("IsSeries=@IsSeries"); + cmd.Parameters.Add(cmd, "@IsSeries", DbType.Boolean).Value = query.IsSeries; + } + if (query.IsNews.HasValue) + { + whereClauses.Add("IsNews=@IsNews"); + cmd.Parameters.Add(cmd, "@IsNews", DbType.Boolean).Value = query.IsNews; + } + if (query.IsKids.HasValue) + { + whereClauses.Add("IsKids=@IsKids"); + cmd.Parameters.Add(cmd, "@IsKids", DbType.Boolean).Value = query.IsKids; + } + if (query.IsSports.HasValue) + { + whereClauses.Add("IsSports=@IsSports"); + cmd.Parameters.Add(cmd, "@IsSports", DbType.Boolean).Value = query.IsSports; + } + } + else + { + var programAttribtues = new List(); + if (query.IsMovie ?? false) + { + var alternateTypes = new List(); + if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Movie).Name)) + { + alternateTypes.Add(typeof(Movie).FullName); + } + if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Trailer).Name)) + { + alternateTypes.Add(typeof(Trailer).FullName); + } + + if (alternateTypes.Count == 0) + { + programAttribtues.Add("IsMovie=@IsMovie"); + } + else + { + programAttribtues.Add("(IsMovie is null OR IsMovie=@IsMovie)"); + } + + cmd.Parameters.Add(cmd, "@IsMovie", DbType.Boolean).Value = true; + } + if (query.IsSports ?? false) + { + programAttribtues.Add("IsSports=@IsSports"); + cmd.Parameters.Add(cmd, "@IsSports", DbType.Boolean).Value = true; + } + if (query.IsNews ?? false) + { + programAttribtues.Add("IsNews=@IsNews"); + cmd.Parameters.Add(cmd, "@IsNews", DbType.Boolean).Value = true; + } + if (query.IsSeries ?? false) + { + programAttribtues.Add("IsSeries=@IsSeries"); + cmd.Parameters.Add(cmd, "@IsSeries", DbType.Boolean).Value = true; + } + if (query.IsKids ?? false) + { + programAttribtues.Add("IsKids=@IsKids"); + cmd.Parameters.Add(cmd, "@IsKids", DbType.Boolean).Value = true; + } + if (programAttribtues.Count > 0) + { + whereClauses.Add("(" + string.Join(" OR ", programAttribtues.ToArray()) + ")"); + } + } + + if (query.SimilarTo != null) + { + whereClauses.Add("SimilarityScore > 0"); + } + + if (query.IsFolder.HasValue) + { + whereClauses.Add("IsFolder=@IsFolder"); + cmd.Parameters.Add(cmd, "@IsFolder", DbType.Boolean).Value = query.IsFolder; + } + + var includeTypes = query.IncludeItemTypes.SelectMany(MapIncludeItemTypes).ToArray(); + if (includeTypes.Length == 1) + { + whereClauses.Add("type=@type" + paramSuffix); + cmd.Parameters.Add(cmd, "@type" + paramSuffix, DbType.String).Value = includeTypes[0]; + } + else if (includeTypes.Length > 1) + { + var inClause = string.Join(",", includeTypes.Select(i => "'" + i + "'").ToArray()); + whereClauses.Add(string.Format("type in ({0})", inClause)); + } + + var excludeTypes = query.ExcludeItemTypes.SelectMany(MapIncludeItemTypes).ToArray(); + if (excludeTypes.Length == 1) + { + whereClauses.Add("type<>@type"); + cmd.Parameters.Add(cmd, "@type", DbType.String).Value = excludeTypes[0]; + } + else if (excludeTypes.Length > 1) + { + var inClause = string.Join(",", excludeTypes.Select(i => "'" + i + "'").ToArray()); + whereClauses.Add(string.Format("type not in ({0})", inClause)); + } + + if (query.ChannelIds.Length == 1) + { + whereClauses.Add("ChannelId=@ChannelId"); + cmd.Parameters.Add(cmd, "@ChannelId", DbType.String).Value = query.ChannelIds[0]; + } + if (query.ChannelIds.Length > 1) + { + var inClause = string.Join(",", query.ChannelIds.Select(i => "'" + i + "'").ToArray()); + whereClauses.Add(string.Format("ChannelId in ({0})", inClause)); + } + + if (query.ParentId.HasValue) + { + whereClauses.Add("ParentId=@ParentId"); + cmd.Parameters.Add(cmd, "@ParentId", DbType.Guid).Value = query.ParentId.Value; + } + + if (!string.IsNullOrWhiteSpace(query.Path)) + { + whereClauses.Add("Path=@Path"); + cmd.Parameters.Add(cmd, "@Path", DbType.String).Value = query.Path; + } + + if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey)) + { + whereClauses.Add("PresentationUniqueKey=@PresentationUniqueKey"); + cmd.Parameters.Add(cmd, "@PresentationUniqueKey", DbType.String).Value = query.PresentationUniqueKey; + } + + if (query.MinCommunityRating.HasValue) + { + whereClauses.Add("CommunityRating>=@MinCommunityRating"); + cmd.Parameters.Add(cmd, "@MinCommunityRating", DbType.Double).Value = query.MinCommunityRating.Value; + } + + if (query.MinIndexNumber.HasValue) + { + whereClauses.Add("IndexNumber>=@MinIndexNumber"); + cmd.Parameters.Add(cmd, "@MinIndexNumber", DbType.Int32).Value = query.MinIndexNumber.Value; + } + + if (query.MinDateCreated.HasValue) + { + whereClauses.Add("DateCreated>=@MinDateCreated"); + cmd.Parameters.Add(cmd, "@MinDateCreated", DbType.DateTime).Value = query.MinDateCreated.Value; + } + + if (query.MinDateLastSaved.HasValue) + { + whereClauses.Add("DateLastSaved>=@MinDateLastSaved"); + cmd.Parameters.Add(cmd, "@MinDateLastSaved", DbType.DateTime).Value = query.MinDateLastSaved.Value; + } + + //if (query.MinPlayers.HasValue) + //{ + // whereClauses.Add("Players>=@MinPlayers"); + // cmd.Parameters.Add(cmd, "@MinPlayers", DbType.Int32).Value = query.MinPlayers.Value; + //} + + //if (query.MaxPlayers.HasValue) + //{ + // whereClauses.Add("Players<=@MaxPlayers"); + // cmd.Parameters.Add(cmd, "@MaxPlayers", DbType.Int32).Value = query.MaxPlayers.Value; + //} + + if (query.IndexNumber.HasValue) + { + whereClauses.Add("IndexNumber=@IndexNumber"); + cmd.Parameters.Add(cmd, "@IndexNumber", DbType.Int32).Value = query.IndexNumber.Value; + } + if (query.ParentIndexNumber.HasValue) + { + whereClauses.Add("ParentIndexNumber=@ParentIndexNumber"); + cmd.Parameters.Add(cmd, "@ParentIndexNumber", DbType.Int32).Value = query.ParentIndexNumber.Value; + } + if (query.ParentIndexNumberNotEquals.HasValue) + { + whereClauses.Add("(ParentIndexNumber<>@ParentIndexNumberNotEquals or ParentIndexNumber is null)"); + cmd.Parameters.Add(cmd, "@ParentIndexNumberNotEquals", DbType.Int32).Value = query.ParentIndexNumberNotEquals.Value; + } + if (query.MinEndDate.HasValue) + { + whereClauses.Add("EndDate>=@MinEndDate"); + cmd.Parameters.Add(cmd, "@MinEndDate", DbType.Date).Value = query.MinEndDate.Value; + } + + if (query.MaxEndDate.HasValue) + { + whereClauses.Add("EndDate<=@MaxEndDate"); + cmd.Parameters.Add(cmd, "@MaxEndDate", DbType.Date).Value = query.MaxEndDate.Value; + } + + if (query.MinStartDate.HasValue) + { + whereClauses.Add("StartDate>=@MinStartDate"); + cmd.Parameters.Add(cmd, "@MinStartDate", DbType.Date).Value = query.MinStartDate.Value; + } + + if (query.MaxStartDate.HasValue) + { + whereClauses.Add("StartDate<=@MaxStartDate"); + cmd.Parameters.Add(cmd, "@MaxStartDate", DbType.Date).Value = query.MaxStartDate.Value; + } + + if (query.MinPremiereDate.HasValue) + { + whereClauses.Add("PremiereDate>=@MinPremiereDate"); + cmd.Parameters.Add(cmd, "@MinPremiereDate", DbType.Date).Value = query.MinPremiereDate.Value; + } + if (query.MaxPremiereDate.HasValue) + { + whereClauses.Add("PremiereDate<=@MaxPremiereDate"); + cmd.Parameters.Add(cmd, "@MaxPremiereDate", DbType.Date).Value = query.MaxPremiereDate.Value; + } + + if (query.SourceTypes.Length == 1) + { + whereClauses.Add("SourceType=@SourceType"); + cmd.Parameters.Add(cmd, "@SourceType", DbType.String).Value = query.SourceTypes[0]; + } + else if (query.SourceTypes.Length > 1) + { + var inClause = string.Join(",", query.SourceTypes.Select(i => "'" + i + "'").ToArray()); + whereClauses.Add(string.Format("SourceType in ({0})", inClause)); + } + + if (query.ExcludeSourceTypes.Length == 1) + { + whereClauses.Add("SourceType<>@SourceType"); + cmd.Parameters.Add(cmd, "@SourceType", DbType.String).Value = query.SourceTypes[0]; + } + else if (query.ExcludeSourceTypes.Length > 1) + { + var inClause = string.Join(",", query.ExcludeSourceTypes.Select(i => "'" + i + "'").ToArray()); + whereClauses.Add(string.Format("SourceType not in ({0})", inClause)); + } + + if (query.TrailerTypes.Length > 0) + { + var clauses = new List(); + var index = 0; + foreach (var type in query.TrailerTypes) + { + clauses.Add("TrailerTypes like @TrailerTypes" + index); + cmd.Parameters.Add(cmd, "@TrailerTypes" + index, DbType.String).Value = "%" + type + "%"; + index++; + } + var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; + whereClauses.Add(clause); + } + + if (query.IsAiring.HasValue) + { + if (query.IsAiring.Value) + { + whereClauses.Add("StartDate<=@MaxStartDate"); + cmd.Parameters.Add(cmd, "@MaxStartDate", DbType.Date).Value = DateTime.UtcNow; + + whereClauses.Add("EndDate>=@MinEndDate"); + cmd.Parameters.Add(cmd, "@MinEndDate", DbType.Date).Value = DateTime.UtcNow; + } + else + { + whereClauses.Add("(StartDate>@IsAiringDate OR EndDate < @IsAiringDate)"); + cmd.Parameters.Add(cmd, "@IsAiringDate", DbType.Date).Value = DateTime.UtcNow; + } + } + + if (query.PersonIds.Length > 0) + { + // Todo: improve without having to do this + query.Person = query.PersonIds.Select(i => RetrieveItem(new Guid(i))).Where(i => i != null).Select(i => i.Name).FirstOrDefault(); + } + + if (!string.IsNullOrWhiteSpace(query.Person)) + { + whereClauses.Add("Guid in (select ItemId from People where Name=@PersonName)"); + cmd.Parameters.Add(cmd, "@PersonName", DbType.String).Value = query.Person; + } + + if (!string.IsNullOrWhiteSpace(query.SlugName)) + { + whereClauses.Add("SlugName=@SlugName"); + cmd.Parameters.Add(cmd, "@SlugName", DbType.String).Value = query.SlugName; + } + + if (!string.IsNullOrWhiteSpace(query.MinSortName)) + { + whereClauses.Add("SortName>=@MinSortName"); + cmd.Parameters.Add(cmd, "@MinSortName", DbType.String).Value = query.MinSortName; + } + + if (!string.IsNullOrWhiteSpace(query.ExternalSeriesId)) + { + whereClauses.Add("ExternalSeriesId=@ExternalSeriesId"); + cmd.Parameters.Add(cmd, "@ExternalSeriesId", DbType.String).Value = query.ExternalSeriesId; + } + + if (!string.IsNullOrWhiteSpace(query.Name)) + { + whereClauses.Add("CleanName=@Name"); + cmd.Parameters.Add(cmd, "@Name", DbType.String).Value = GetCleanValue(query.Name); + } + + if (!string.IsNullOrWhiteSpace(query.NameContains)) + { + whereClauses.Add("CleanName like @NameContains"); + cmd.Parameters.Add(cmd, "@NameContains", DbType.String).Value = "%" + GetCleanValue(query.NameContains) + "%"; + } + if (!string.IsNullOrWhiteSpace(query.NameStartsWith)) + { + whereClauses.Add("SortName like @NameStartsWith"); + cmd.Parameters.Add(cmd, "@NameStartsWith", DbType.String).Value = query.NameStartsWith + "%"; + } + if (!string.IsNullOrWhiteSpace(query.NameStartsWithOrGreater)) + { + whereClauses.Add("SortName >= @NameStartsWithOrGreater"); + // lowercase this because SortName is stored as lowercase + cmd.Parameters.Add(cmd, "@NameStartsWithOrGreater", DbType.String).Value = query.NameStartsWithOrGreater.ToLower(); + } + if (!string.IsNullOrWhiteSpace(query.NameLessThan)) + { + whereClauses.Add("SortName < @NameLessThan"); + // lowercase this because SortName is stored as lowercase + cmd.Parameters.Add(cmd, "@NameLessThan", DbType.String).Value = query.NameLessThan.ToLower(); + } + + if (query.ImageTypes.Length > 0 && _config.Configuration.SchemaVersion >= 87) + { + var requiredImageIndex = 0; + + foreach (var requiredImage in query.ImageTypes) + { + var paramName = "@RequiredImageType" + requiredImageIndex; + whereClauses.Add("(select path from images where ItemId=Guid and ImageType=" + paramName + " limit 1) not null"); + cmd.Parameters.Add(cmd, paramName, DbType.Int32).Value = (int)requiredImage; + requiredImageIndex++; + } + } + + if (query.IsLiked.HasValue) + { + if (query.IsLiked.Value) + { + whereClauses.Add("rating>=@UserRating"); + cmd.Parameters.Add(cmd, "@UserRating", DbType.Double).Value = UserItemData.MinLikeValue; + } + else + { + whereClauses.Add("(rating is null or rating<@UserRating)"); + cmd.Parameters.Add(cmd, "@UserRating", DbType.Double).Value = UserItemData.MinLikeValue; + } + } + + if (query.IsFavoriteOrLiked.HasValue) + { + if (query.IsFavoriteOrLiked.Value) + { + whereClauses.Add("IsFavorite=@IsFavoriteOrLiked"); + } + else + { + whereClauses.Add("(IsFavorite is null or IsFavorite=@IsFavoriteOrLiked)"); + } + cmd.Parameters.Add(cmd, "@IsFavoriteOrLiked", DbType.Boolean).Value = query.IsFavoriteOrLiked.Value; + } + + if (query.IsFavorite.HasValue) + { + if (query.IsFavorite.Value) + { + whereClauses.Add("IsFavorite=@IsFavorite"); + } + else + { + whereClauses.Add("(IsFavorite is null or IsFavorite=@IsFavorite)"); + } + cmd.Parameters.Add(cmd, "@IsFavorite", DbType.Boolean).Value = query.IsFavorite.Value; + } + + if (EnableJoinUserData(query)) + { + if (query.IsPlayed.HasValue) + { + if (query.IsPlayed.Value) + { + whereClauses.Add("(played=@IsPlayed)"); + } + else + { + whereClauses.Add("(played is null or played=@IsPlayed)"); + } + cmd.Parameters.Add(cmd, "@IsPlayed", DbType.Boolean).Value = query.IsPlayed.Value; + } + } + + if (query.IsResumable.HasValue) + { + if (query.IsResumable.Value) + { + whereClauses.Add("playbackPositionTicks > 0"); + } + else + { + whereClauses.Add("(playbackPositionTicks is null or playbackPositionTicks = 0)"); + } + } + + if (query.ArtistNames.Length > 0) + { + var clauses = new List(); + var index = 0; + foreach (var artist in query.ArtistNames) + { + clauses.Add("@ArtistName" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type <= 1)"); + cmd.Parameters.Add(cmd, "@ArtistName" + index, DbType.String).Value = GetCleanValue(artist); + index++; + } + var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; + whereClauses.Add(clause); + } + + if (query.ExcludeArtistIds.Length > 0) + { + var clauses = new List(); + var index = 0; + foreach (var artistId in query.ExcludeArtistIds) + { + var artistItem = RetrieveItem(new Guid(artistId)); + if (artistItem != null) + { + clauses.Add("@ExcludeArtistName" + index + " not in (select CleanValue from itemvalues where ItemId=Guid and Type <= 1)"); + cmd.Parameters.Add(cmd, "@ExcludeArtistName" + index, DbType.String).Value = GetCleanValue(artistItem.Name); + index++; + } + } + var clause = "(" + string.Join(" AND ", clauses.ToArray()) + ")"; + whereClauses.Add(clause); + } + + if (query.GenreIds.Length > 0) + { + // Todo: improve without having to do this + query.Genres = query.GenreIds.Select(i => RetrieveItem(new Guid(i))).Where(i => i != null).Select(i => i.Name).ToArray(); + } + + if (query.Genres.Length > 0) + { + var clauses = new List(); + var index = 0; + foreach (var item in query.Genres) + { + clauses.Add("@Genre" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type=2)"); + cmd.Parameters.Add(cmd, "@Genre" + index, DbType.String).Value = GetCleanValue(item); + index++; + } + var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; + whereClauses.Add(clause); + } + + if (query.Tags.Length > 0) + { + var clauses = new List(); + var index = 0; + foreach (var item in query.Tags) + { + clauses.Add("@Tag" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type=4)"); + cmd.Parameters.Add(cmd, "@Tag" + index, DbType.String).Value = GetCleanValue(item); + index++; + } + var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; + whereClauses.Add(clause); + } + + if (query.StudioIds.Length > 0) + { + // Todo: improve without having to do this + query.Studios = query.StudioIds.Select(i => RetrieveItem(new Guid(i))).Where(i => i != null).Select(i => i.Name).ToArray(); + } + + if (query.Studios.Length > 0) + { + var clauses = new List(); + var index = 0; + foreach (var item in query.Studios) + { + clauses.Add("@Studio" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type=3)"); + cmd.Parameters.Add(cmd, "@Studio" + index, DbType.String).Value = GetCleanValue(item); + index++; + } + var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; + whereClauses.Add(clause); + } + + if (query.Keywords.Length > 0) + { + var clauses = new List(); + var index = 0; + foreach (var item in query.Keywords) + { + clauses.Add("@Keyword" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type=5)"); + cmd.Parameters.Add(cmd, "@Keyword" + index, DbType.String).Value = GetCleanValue(item); + index++; + } + var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; + whereClauses.Add(clause); + } + + if (query.OfficialRatings.Length > 0) + { + var clauses = new List(); + var index = 0; + foreach (var item in query.OfficialRatings) + { + clauses.Add("OfficialRating=@OfficialRating" + index); + cmd.Parameters.Add(cmd, "@OfficialRating" + index, DbType.String).Value = item; + index++; + } + var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; + whereClauses.Add(clause); + } + + if (query.MinParentalRating.HasValue) + { + whereClauses.Add("InheritedParentalRatingValue<=@MinParentalRating"); + cmd.Parameters.Add(cmd, "@MinParentalRating", DbType.Int32).Value = query.MinParentalRating.Value; + } + + if (query.MaxParentalRating.HasValue) + { + whereClauses.Add("InheritedParentalRatingValue<=@MaxParentalRating"); + cmd.Parameters.Add(cmd, "@MaxParentalRating", DbType.Int32).Value = query.MaxParentalRating.Value; + } + + if (query.HasParentalRating.HasValue) + { + if (query.HasParentalRating.Value) + { + whereClauses.Add("InheritedParentalRatingValue > 0"); + } + else + { + whereClauses.Add("InheritedParentalRatingValue = 0"); + } + } + + if (query.HasOverview.HasValue) + { + if (query.HasOverview.Value) + { + whereClauses.Add("(Overview not null AND Overview<>'')"); + } + else + { + whereClauses.Add("(Overview is null OR Overview='')"); + } + } + + if (query.HasDeadParentId.HasValue) + { + if (query.HasDeadParentId.Value) + { + whereClauses.Add("ParentId NOT NULL AND ParentId NOT IN (select guid from TypedBaseItems)"); + } + } + + if (query.Years.Length == 1) + { + whereClauses.Add("ProductionYear=@Years"); + cmd.Parameters.Add(cmd, "@Years", DbType.Int32).Value = query.Years[0].ToString(); + } + else if (query.Years.Length > 1) + { + var val = string.Join(",", query.Years.ToArray()); + + whereClauses.Add("ProductionYear in (" + val + ")"); + } + + if (query.LocationTypes.Length == 1) + { + if (query.LocationTypes[0] == LocationType.Virtual && _config.Configuration.SchemaVersion >= 90) + { + query.IsVirtualItem = true; + } + else + { + whereClauses.Add("LocationType=@LocationType"); + cmd.Parameters.Add(cmd, "@LocationType", DbType.String).Value = query.LocationTypes[0].ToString(); + } + } + else if (query.LocationTypes.Length > 1) + { + var val = string.Join(",", query.LocationTypes.Select(i => "'" + i + "'").ToArray()); + + whereClauses.Add("LocationType in (" + val + ")"); + } + if (query.ExcludeLocationTypes.Length == 1) + { + if (query.ExcludeLocationTypes[0] == LocationType.Virtual && _config.Configuration.SchemaVersion >= 90) + { + query.IsVirtualItem = false; + } + else + { + whereClauses.Add("LocationType<>@ExcludeLocationTypes"); + cmd.Parameters.Add(cmd, "@ExcludeLocationTypes", DbType.String).Value = query.ExcludeLocationTypes[0].ToString(); + } + } + else if (query.ExcludeLocationTypes.Length > 1) + { + var val = string.Join(",", query.ExcludeLocationTypes.Select(i => "'" + i + "'").ToArray()); + + whereClauses.Add("LocationType not in (" + val + ")"); + } + if (query.IsVirtualItem.HasValue) + { + if (_config.Configuration.SchemaVersion >= 90) + { + whereClauses.Add("IsVirtualItem=@IsVirtualItem"); + cmd.Parameters.Add(cmd, "@IsVirtualItem", DbType.Boolean).Value = query.IsVirtualItem.Value; + } + else if (!query.IsVirtualItem.Value) + { + whereClauses.Add("LocationType<>'Virtual'"); + } + } + if (query.IsSpecialSeason.HasValue) + { + if (query.IsSpecialSeason.Value) + { + whereClauses.Add("IndexNumber = 0"); + } + else + { + whereClauses.Add("IndexNumber <> 0"); + } + } + if (query.IsUnaired.HasValue) + { + if (query.IsUnaired.Value) + { + whereClauses.Add("PremiereDate >= DATETIME('now')"); + } + else + { + whereClauses.Add("PremiereDate < DATETIME('now')"); + } + } + if (query.IsMissing.HasValue && _config.Configuration.SchemaVersion >= 90) + { + if (query.IsMissing.Value) + { + whereClauses.Add("(IsVirtualItem=1 AND PremiereDate < DATETIME('now'))"); + } + else + { + whereClauses.Add("(IsVirtualItem=0 OR PremiereDate >= DATETIME('now'))"); + } + } + if (query.IsVirtualUnaired.HasValue && _config.Configuration.SchemaVersion >= 90) + { + if (query.IsVirtualUnaired.Value) + { + whereClauses.Add("(IsVirtualItem=1 AND PremiereDate >= DATETIME('now'))"); + } + else + { + whereClauses.Add("(IsVirtualItem=0 OR PremiereDate < DATETIME('now'))"); + } + } + if (query.MediaTypes.Length == 1) + { + whereClauses.Add("MediaType=@MediaTypes"); + cmd.Parameters.Add(cmd, "@MediaTypes", DbType.String).Value = query.MediaTypes[0]; + } + if (query.MediaTypes.Length > 1) + { + var val = string.Join(",", query.MediaTypes.Select(i => "'" + i + "'").ToArray()); + + whereClauses.Add("MediaType in (" + val + ")"); + } + if (query.ItemIds.Length > 0) + { + var includeIds = new List(); + + var index = 0; + foreach (var id in query.ItemIds) + { + includeIds.Add("Guid = @IncludeId" + index); + cmd.Parameters.Add(cmd, "@IncludeId" + index, DbType.Guid).Value = new Guid(id); + index++; + } + + whereClauses.Add(string.Join(" OR ", includeIds.ToArray())); + } + if (query.ExcludeItemIds.Length > 0) + { + var excludeIds = new List(); + + var index = 0; + foreach (var id in query.ExcludeItemIds) + { + excludeIds.Add("Guid <> @ExcludeId" + index); + cmd.Parameters.Add(cmd, "@ExcludeId" + index, DbType.Guid).Value = new Guid(id); + index++; + } + + whereClauses.Add(string.Join(" AND ", excludeIds.ToArray())); + } + + if (query.ExcludeProviderIds.Count > 0) + { + var excludeIds = new List(); + + var index = 0; + foreach (var pair in query.ExcludeProviderIds) + { + if (string.Equals(pair.Key, MetadataProviders.TmdbCollection.ToString(), StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var paramName = "@ExcludeProviderId" + index; + excludeIds.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = '" + pair.Key + "'), '') <> " + paramName + ")"); + cmd.Parameters.Add(cmd, paramName, DbType.String).Value = pair.Value; + index++; + } + + whereClauses.Add(string.Join(" AND ", excludeIds.ToArray())); + } + + if (query.HasImdbId.HasValue) + { + var fn = query.HasImdbId.Value ? "<>" : "="; + whereClauses.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = 'Imdb'), '') " + fn + " '')"); + } + + if (query.HasTmdbId.HasValue) + { + var fn = query.HasTmdbId.Value ? "<>" : "="; + whereClauses.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = 'Tmdb'), '') " + fn + " '')"); + } + + if (query.HasTvdbId.HasValue) + { + var fn = query.HasTvdbId.Value ? "<>" : "="; + whereClauses.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = 'Tvdb'), '') " + fn + " '')"); + } + + if (query.AlbumNames.Length > 0) + { + var clause = "("; + + var index = 0; + foreach (var name in query.AlbumNames) + { + if (index > 0) + { + clause += " OR "; + } + clause += "Album=@AlbumName" + index; + cmd.Parameters.Add(cmd, "@AlbumName" + index, DbType.String).Value = name; + index++; + } + + clause += ")"; + whereClauses.Add(clause); + } + if (query.HasThemeSong.HasValue) + { + if (query.HasThemeSong.Value) + { + whereClauses.Add("ThemeSongIds not null"); + } + else + { + whereClauses.Add("ThemeSongIds is null"); + } + } + if (query.HasThemeVideo.HasValue) + { + if (query.HasThemeVideo.Value) + { + whereClauses.Add("ThemeVideoIds not null"); + } + else + { + whereClauses.Add("ThemeVideoIds is null"); + } + } + + //var enableItemsByName = query.IncludeItemsByName ?? query.IncludeItemTypes.Length > 0; + var enableItemsByName = query.IncludeItemsByName ?? false; + + if (query.TopParentIds.Length == 1) + { + if (enableItemsByName) + { + whereClauses.Add("(TopParentId=@TopParentId or IsItemByName=@IsItemByName)"); + cmd.Parameters.Add(cmd, "@IsItemByName", DbType.Boolean).Value = true; + } + else + { + whereClauses.Add("(TopParentId=@TopParentId)"); + } + cmd.Parameters.Add(cmd, "@TopParentId", DbType.String).Value = query.TopParentIds[0]; + } + if (query.TopParentIds.Length > 1) + { + var val = string.Join(",", query.TopParentIds.Select(i => "'" + i + "'").ToArray()); + + if (enableItemsByName) + { + whereClauses.Add("(IsItemByName=@IsItemByName or TopParentId in (" + val + "))"); + cmd.Parameters.Add(cmd, "@IsItemByName", DbType.Boolean).Value = true; + } + else + { + whereClauses.Add("(TopParentId in (" + val + "))"); + } + } + + if (query.AncestorIds.Length == 1) + { + whereClauses.Add("Guid in (select itemId from AncestorIds where AncestorId=@AncestorId)"); + cmd.Parameters.Add(cmd, "@AncestorId", DbType.Guid).Value = new Guid(query.AncestorIds[0]); + } + if (query.AncestorIds.Length > 1) + { + var inClause = string.Join(",", query.AncestorIds.Select(i => "'" + new Guid(i).ToString("N") + "'").ToArray()); + whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause)); + } + if (!string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey)) + { + var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey"; + whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause)); + cmd.Parameters.Add(cmd, "@AncestorWithPresentationUniqueKey", DbType.String).Value = query.AncestorWithPresentationUniqueKey; + } + + if (query.BlockUnratedItems.Length == 1) + { + whereClauses.Add("(InheritedParentalRatingValue > 0 or UnratedType <> @UnratedType)"); + cmd.Parameters.Add(cmd, "@UnratedType", DbType.String).Value = query.BlockUnratedItems[0].ToString(); + } + if (query.BlockUnratedItems.Length > 1) + { + var inClause = string.Join(",", query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'").ToArray()); + whereClauses.Add(string.Format("(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))", inClause)); + } + + var excludeTagIndex = 0; + foreach (var excludeTag in query.ExcludeTags) + { + whereClauses.Add("(Tags is null OR Tags not like @excludeTag" + excludeTagIndex + ")"); + cmd.Parameters.Add(cmd, "@excludeTag" + excludeTagIndex, DbType.String).Value = "%" + excludeTag + "%"; + excludeTagIndex++; + } + + excludeTagIndex = 0; + foreach (var excludeTag in query.ExcludeInheritedTags) + { + whereClauses.Add("(InheritedTags is null OR InheritedTags not like @excludeInheritedTag" + excludeTagIndex + ")"); + cmd.Parameters.Add(cmd, "@excludeInheritedTag" + excludeTagIndex, DbType.String).Value = "%" + excludeTag + "%"; + excludeTagIndex++; + } + + return whereClauses; + } + + private string GetCleanValue(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return value; + } + + return value.RemoveDiacritics().ToLower(); + } + + private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query) + { + if (!query.GroupByPresentationUniqueKey) + { + return false; + } + + if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey)) + { + return false; + } + + if (query.User == null) + { + return false; + } + + if (query.IncludeItemTypes.Length == 0) + { + return true; + } + + var types = new[] { + typeof(Episode).Name, + typeof(Video).Name , + typeof(Movie).Name , + typeof(MusicVideo).Name , + typeof(Series).Name , + typeof(Season).Name }; + + if (types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase))) + { + return true; + } + + return false; + } + + private static readonly Type[] KnownTypes = + { + typeof(LiveTvProgram), + typeof(LiveTvChannel), + typeof(LiveTvVideoRecording), + typeof(LiveTvAudioRecording), + typeof(Series), + typeof(Audio), + typeof(MusicAlbum), + typeof(MusicArtist), + typeof(MusicGenre), + typeof(MusicVideo), + typeof(Movie), + typeof(Playlist), + typeof(AudioPodcast), + typeof(Trailer), + typeof(BoxSet), + typeof(Episode), + typeof(Season), + typeof(Series), + typeof(Book), + typeof(CollectionFolder), + typeof(Folder), + typeof(Game), + typeof(GameGenre), + typeof(GameSystem), + typeof(Genre), + typeof(Person), + typeof(Photo), + typeof(PhotoAlbum), + typeof(Studio), + typeof(UserRootFolder), + typeof(UserView), + typeof(Video), + typeof(Year), + typeof(Channel), + typeof(AggregateFolder) + }; + + public async Task UpdateInheritedValues(CancellationToken cancellationToken) + { + await UpdateInheritedTags(cancellationToken).ConfigureAwait(false); + } + + private async Task UpdateInheritedTags(CancellationToken cancellationToken) + { + var newValues = new List>(); + + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "select Guid,InheritedTags,(select group_concat(Tags, '|') from TypedBaseItems where (guid=outer.guid) OR (guid in (Select AncestorId from AncestorIds where ItemId=Outer.guid))) as NewInheritedTags from typedbaseitems as Outer where NewInheritedTags <> InheritedTags"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + var id = reader.GetGuid(0); + string value = reader.IsDBNull(2) ? null : reader.GetString(2); + + newValues.Add(new Tuple(id, value)); + } + } + } + + Logger.Debug("UpdateInheritedTags - {0} rows", newValues.Count); + if (newValues.Count == 0) + { + return; + } + + await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + IDbTransaction transaction = null; + + try + { + transaction = _connection.BeginTransaction(); + + foreach (var item in newValues) + { + _updateInheritedTagsCommand.GetParameter(0).Value = item.Item1; + _updateInheritedTagsCommand.GetParameter(1).Value = item.Item2; + + _updateInheritedTagsCommand.Transaction = transaction; + _updateInheritedTagsCommand.ExecuteNonQuery(); + } + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Error running query:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + + WriteLock.Release(); + } + } + + private static Dictionary GetTypeMapDictionary() + { + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var t in KnownTypes) + { + dict[t.Name] = new[] { t.FullName }; + } + + dict["Recording"] = new[] { typeof(LiveTvAudioRecording).FullName, typeof(LiveTvVideoRecording).FullName }; + dict["Program"] = new[] { typeof(LiveTvProgram).FullName }; + dict["TvChannel"] = new[] { typeof(LiveTvChannel).FullName }; + + return dict; + } + + // Not crazy about having this all the way down here, but at least it's in one place + readonly Dictionary _types = GetTypeMapDictionary(); + + private IEnumerable MapIncludeItemTypes(string value) + { + string[] result; + if (_types.TryGetValue(value, out result)) + { + return result; + } + + return new[] { value }; + } + + public async Task DeleteItem(Guid id, CancellationToken cancellationToken) + { + if (id == Guid.Empty) + { + throw new ArgumentNullException("id"); + } + + CheckDisposed(); + + await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + IDbTransaction transaction = null; + + try + { + transaction = _connection.BeginTransaction(); + + // Delete people + _deletePeopleCommand.GetParameter(0).Value = id; + _deletePeopleCommand.Transaction = transaction; + _deletePeopleCommand.ExecuteNonQuery(); + + // Delete chapters + _deleteChaptersCommand.GetParameter(0).Value = id; + _deleteChaptersCommand.Transaction = transaction; + _deleteChaptersCommand.ExecuteNonQuery(); + + // Delete media streams + _deleteStreamsCommand.GetParameter(0).Value = id; + _deleteStreamsCommand.Transaction = transaction; + _deleteStreamsCommand.ExecuteNonQuery(); + + // Delete ancestors + _deleteAncestorsCommand.GetParameter(0).Value = id; + _deleteAncestorsCommand.Transaction = transaction; + _deleteAncestorsCommand.ExecuteNonQuery(); + + // Delete item values + _deleteItemValuesCommand.GetParameter(0).Value = id; + _deleteItemValuesCommand.Transaction = transaction; + _deleteItemValuesCommand.ExecuteNonQuery(); + + // Delete provider ids + _deleteProviderIdsCommand.GetParameter(0).Value = id; + _deleteProviderIdsCommand.Transaction = transaction; + _deleteProviderIdsCommand.ExecuteNonQuery(); + + // Delete images + _deleteImagesCommand.GetParameter(0).Value = id; + _deleteImagesCommand.Transaction = transaction; + _deleteImagesCommand.ExecuteNonQuery(); + + // Delete the item + _deleteItemCommand.GetParameter(0).Value = id; + _deleteItemCommand.Transaction = transaction; + _deleteItemCommand.ExecuteNonQuery(); + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save children:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + + WriteLock.Release(); + } + } + + public List GetPeopleNames(InternalPeopleQuery query) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + CheckDisposed(); + + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "select Distinct Name from People"; + + var whereClauses = GetPeopleWhereClauses(query, cmd); + + if (whereClauses.Count > 0) + { + cmd.CommandText += " where " + string.Join(" AND ", whereClauses.ToArray()); + } + + cmd.CommandText += " order by ListOrder"; + + var list = new List(); + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + list.Add(reader.GetString(0)); + } + } + + return list; + } + } + + public List GetPeople(InternalPeopleQuery query) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + CheckDisposed(); + + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "select ItemId, Name, Role, PersonType, SortOrder from People"; + + var whereClauses = GetPeopleWhereClauses(query, cmd); + + if (whereClauses.Count > 0) + { + cmd.CommandText += " where " + string.Join(" AND ", whereClauses.ToArray()); + } + + cmd.CommandText += " order by ListOrder"; + + var list = new List(); + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + list.Add(GetPerson(reader)); + } + } + + return list; + } + } + + private List GetPeopleWhereClauses(InternalPeopleQuery query, IDbCommand cmd) + { + var whereClauses = new List(); + + if (query.ItemId != Guid.Empty) + { + whereClauses.Add("ItemId=@ItemId"); + cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = query.ItemId; + } + if (query.AppearsInItemId != Guid.Empty) + { + whereClauses.Add("Name in (Select Name from People where ItemId=@AppearsInItemId)"); + cmd.Parameters.Add(cmd, "@AppearsInItemId", DbType.Guid).Value = query.AppearsInItemId; + } + if (query.PersonTypes.Count == 1) + { + whereClauses.Add("PersonType=@PersonType"); + cmd.Parameters.Add(cmd, "@PersonType", DbType.String).Value = query.PersonTypes[0]; + } + if (query.PersonTypes.Count > 1) + { + var val = string.Join(",", query.PersonTypes.Select(i => "'" + i + "'").ToArray()); + + whereClauses.Add("PersonType in (" + val + ")"); + } + if (query.ExcludePersonTypes.Count == 1) + { + whereClauses.Add("PersonType<>@PersonType"); + cmd.Parameters.Add(cmd, "@PersonType", DbType.String).Value = query.ExcludePersonTypes[0]; + } + if (query.ExcludePersonTypes.Count > 1) + { + var val = string.Join(",", query.ExcludePersonTypes.Select(i => "'" + i + "'").ToArray()); + + whereClauses.Add("PersonType not in (" + val + ")"); + } + if (query.MaxListOrder.HasValue) + { + whereClauses.Add("ListOrder<=@MaxListOrder"); + cmd.Parameters.Add(cmd, "@MaxListOrder", DbType.Int32).Value = query.MaxListOrder.Value; + } + if (!string.IsNullOrWhiteSpace(query.NameContains)) + { + whereClauses.Add("Name like @NameContains"); + cmd.Parameters.Add(cmd, "@NameContains", DbType.String).Value = "%" + query.NameContains + "%"; + } + if (query.SourceTypes.Length == 1) + { + whereClauses.Add("(select sourcetype from typedbaseitems where guid=ItemId) = @SourceTypes"); + cmd.Parameters.Add(cmd, "@SourceTypes", DbType.String).Value = query.SourceTypes[0].ToString(); + } + + return whereClauses; + } + + private void UpdateAncestors(Guid itemId, List ancestorIds, IDbTransaction transaction) + { + if (itemId == Guid.Empty) + { + throw new ArgumentNullException("itemId"); + } + + if (ancestorIds == null) + { + throw new ArgumentNullException("ancestorIds"); + } + + CheckDisposed(); + + // First delete + _deleteAncestorsCommand.GetParameter(0).Value = itemId; + _deleteAncestorsCommand.Transaction = transaction; + + _deleteAncestorsCommand.ExecuteNonQuery(); + + foreach (var ancestorId in ancestorIds) + { + _saveAncestorCommand.GetParameter(0).Value = itemId; + _saveAncestorCommand.GetParameter(1).Value = ancestorId; + _saveAncestorCommand.GetParameter(2).Value = ancestorId.ToString("N"); + + _saveAncestorCommand.Transaction = transaction; + + _saveAncestorCommand.ExecuteNonQuery(); + } + } + + public QueryResult> GetAllArtists(InternalItemsQuery query) + { + return GetItemValues(query, new[] { 0, 1 }, typeof(MusicArtist).FullName); + } + + public QueryResult> GetArtists(InternalItemsQuery query) + { + return GetItemValues(query, new[] { 0 }, typeof(MusicArtist).FullName); + } + + public QueryResult> GetAlbumArtists(InternalItemsQuery query) + { + return GetItemValues(query, new[] { 1 }, typeof(MusicArtist).FullName); + } + + public QueryResult> GetStudios(InternalItemsQuery query) + { + return GetItemValues(query, new[] { 3 }, typeof(Studio).FullName); + } + + public QueryResult> GetGenres(InternalItemsQuery query) + { + return GetItemValues(query, new[] { 2 }, typeof(Genre).FullName); + } + + public QueryResult> GetGameGenres(InternalItemsQuery query) + { + return GetItemValues(query, new[] { 2 }, typeof(GameGenre).FullName); + } + + public QueryResult> GetMusicGenres(InternalItemsQuery query) + { + return GetItemValues(query, new[] { 2 }, typeof(MusicGenre).FullName); + } + + public List GetStudioNames() + { + return GetItemValueNames(new[] { 3 }, new List(), new List()); + } + + public List GetAllArtistNames() + { + return GetItemValueNames(new[] { 0, 1 }, new List(), new List()); + } + + public List GetMusicGenreNames() + { + return GetItemValueNames(new[] { 2 }, new List { "Audio", "MusicVideo", "MusicAlbum", "MusicArtist" }, new List()); + } + + public List GetGameGenreNames() + { + return GetItemValueNames(new[] { 2 }, new List { "Game" }, new List()); + } + + public List GetGenreNames() + { + return GetItemValueNames(new[] { 2 }, new List(), new List { "Audio", "MusicVideo", "MusicAlbum", "MusicArtist", "Game", "GameSystem" }); + } + + private List GetItemValueNames(int[] itemValueTypes, List withItemTypes, List excludeItemTypes) + { + CheckDisposed(); + + withItemTypes = withItemTypes.SelectMany(MapIncludeItemTypes).ToList(); + excludeItemTypes = excludeItemTypes.SelectMany(MapIncludeItemTypes).ToList(); + + var now = DateTime.UtcNow; + + var typeClause = itemValueTypes.Length == 1 ? + ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) : + ("Type in (" + string.Join(",", itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture)).ToArray()) + ")"); + + var list = new List(); + + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "Select Value From ItemValues where " + typeClause; + + if (withItemTypes.Count > 0) + { + var typeString = string.Join(",", withItemTypes.Select(i => "'" + i + "'").ToArray()); + cmd.CommandText += " AND ItemId In (select guid from typedbaseitems where type in (" + typeString + "))"; + } + if (excludeItemTypes.Count > 0) + { + var typeString = string.Join(",", excludeItemTypes.Select(i => "'" + i + "'").ToArray()); + cmd.CommandText += " AND ItemId not In (select guid from typedbaseitems where type in (" + typeString + "))"; + } + + cmd.CommandText += " Group By CleanValue"; + + var commandBehavior = CommandBehavior.SequentialAccess | CommandBehavior.SingleResult; + + using (var reader = cmd.ExecuteReader(commandBehavior)) + { + LogQueryTime("GetItemValueNames", cmd, now); + + while (reader.Read()) + { + if (!reader.IsDBNull(0)) + { + list.Add(reader.GetString(0)); + } + } + } + + } + + return list; + } + + private QueryResult> GetItemValues(InternalItemsQuery query, int[] itemValueTypes, string returnType) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + if (!query.Limit.HasValue) + { + query.EnableTotalRecordCount = false; + } + + CheckDisposed(); + + var now = DateTime.UtcNow; + + var typeClause = itemValueTypes.Length == 1 ? + ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) : + ("Type in (" + string.Join(",", itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture)).ToArray()) + ")"); + + using (var cmd = _connection.CreateCommand()) + { + var itemCountColumns = new List>(); + + var typesToCount = query.IncludeItemTypes.ToList(); + + if (typesToCount.Count > 0) + { + var itemCountColumnQuery = "select group_concat(type, '|')" + GetFromText("B"); + + var typeSubQuery = new InternalItemsQuery(query.User) + { + ExcludeItemTypes = query.ExcludeItemTypes, + IncludeItemTypes = query.IncludeItemTypes, + MediaTypes = query.MediaTypes, + AncestorIds = query.AncestorIds, + ExcludeItemIds = query.ExcludeItemIds, + ItemIds = query.ItemIds, + TopParentIds = query.TopParentIds, + ParentId = query.ParentId, + IsPlayed = query.IsPlayed + }; + var whereClauses = GetWhereClauses(typeSubQuery, cmd, "itemTypes"); + + whereClauses.Add("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND " + typeClause + ")"); + + var typeWhereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + itemCountColumnQuery += typeWhereText; + + //itemCountColumnQuery += ")"; + + itemCountColumns.Add(new Tuple("itemTypes", "(" + itemCountColumnQuery + ") as itemTypes")); + } + + var columns = _retriveItemColumns.ToList(); + columns.AddRange(itemCountColumns.Select(i => i.Item2).ToArray()); + + cmd.CommandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, columns.ToArray(), cmd)) + GetFromText(); + cmd.CommandText += GetJoinUserDataText(query); + + var innerQuery = new InternalItemsQuery(query.User) + { + ExcludeItemTypes = query.ExcludeItemTypes, + IncludeItemTypes = query.IncludeItemTypes, + MediaTypes = query.MediaTypes, + AncestorIds = query.AncestorIds, + ExcludeItemIds = query.ExcludeItemIds, + ItemIds = query.ItemIds, + TopParentIds = query.TopParentIds, + ParentId = query.ParentId, + IsPlayed = query.IsPlayed + }; + + var innerWhereClauses = GetWhereClauses(innerQuery, cmd); + + var innerWhereText = innerWhereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", innerWhereClauses.ToArray()); + + var whereText = " where Type=@SelectType"; + + if (typesToCount.Count == 0) + { + whereText += " And CleanName In (Select CleanValue from ItemValues where " + typeClause + " AND ItemId in (select guid from TypedBaseItems" + innerWhereText + "))"; + } + else + { + //whereText += " And itemTypes not null"; + whereText += " And CleanName In (Select CleanValue from ItemValues where " + typeClause + " AND ItemId in (select guid from TypedBaseItems" + innerWhereText + "))"; + } + + var outerQuery = new InternalItemsQuery(query.User) + { + IsFavorite = query.IsFavorite, + IsFavoriteOrLiked = query.IsFavoriteOrLiked, + IsLiked = query.IsLiked, + IsLocked = query.IsLocked, + NameLessThan = query.NameLessThan, + NameStartsWith = query.NameStartsWith, + NameStartsWithOrGreater = query.NameStartsWithOrGreater, + AlbumArtistStartsWithOrGreater = query.AlbumArtistStartsWithOrGreater, + Tags = query.Tags, + OfficialRatings = query.OfficialRatings, + GenreIds = query.GenreIds, + Genres = query.Genres, + Years = query.Years + }; + + var outerWhereClauses = GetWhereClauses(outerQuery, cmd); + + whereText += outerWhereClauses.Count == 0 ? + string.Empty : + " AND " + string.Join(" AND ", outerWhereClauses.ToArray()); + //cmd.CommandText += GetGroupBy(query); + + cmd.CommandText += whereText; + cmd.CommandText += " group by PresentationUniqueKey"; + + cmd.Parameters.Add(cmd, "@SelectType", DbType.String).Value = returnType; + + if (EnableJoinUserData(query)) + { + cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = query.User.Id; + } + + cmd.CommandText += " order by SortName"; + + if (query.Limit.HasValue || query.StartIndex.HasValue) + { + var offset = query.StartIndex ?? 0; + + if (query.Limit.HasValue || offset > 0) + { + cmd.CommandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); + } + + if (offset > 0) + { + cmd.CommandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); + } + } + + cmd.CommandText += ";"; + + var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; + + if (isReturningZeroItems) + { + cmd.CommandText = ""; + } + + if (query.EnableTotalRecordCount) + { + cmd.CommandText += "select count (distinct PresentationUniqueKey)" + GetFromText(); + + cmd.CommandText += GetJoinUserDataText(query); + cmd.CommandText += whereText; + } + else + { + cmd.CommandText = cmd.CommandText.TrimEnd(';'); + } + + var list = new List>(); + var count = 0; + + var commandBehavior = isReturningZeroItems || !query.EnableTotalRecordCount + ? (CommandBehavior.SequentialAccess | CommandBehavior.SingleResult) + : CommandBehavior.SequentialAccess; + + //Logger.Debug("GetItemValues: " + cmd.CommandText); + + using (var reader = cmd.ExecuteReader(commandBehavior)) + { + LogQueryTime("GetItemValues", cmd, now); + + if (isReturningZeroItems) + { + if (reader.Read()) + { + count = reader.GetInt32(0); + } + } + else + { + while (reader.Read()) + { + var item = GetItem(reader); + if (item != null) + { + var countStartColumn = columns.Count - 1; + + list.Add(new Tuple(item, GetItemCounts(reader, countStartColumn, typesToCount))); + } + } + + if (reader.NextResult() && reader.Read()) + { + count = reader.GetInt32(0); + } + } + } + + if (count == 0) + { + count = list.Count; + } + + return new QueryResult> + { + Items = list.ToArray(), + TotalRecordCount = count + }; + + } + } + + private ItemCounts GetItemCounts(IDataReader reader, int countStartColumn, List typesToCount) + { + var counts = new ItemCounts(); + + if (typesToCount.Count == 0) + { + return counts; + } + + var typeString = reader.IsDBNull(countStartColumn) ? null : reader.GetString(countStartColumn); + + if (string.IsNullOrWhiteSpace(typeString)) + { + return counts; + } + + var allTypes = typeString.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) + .ToLookup(i => i).ToList(); + + foreach (var type in allTypes) + { + var value = type.ToList().Count; + var typeName = type.Key; + + if (string.Equals(typeName, typeof(Series).FullName, StringComparison.OrdinalIgnoreCase)) + { + counts.SeriesCount = value; + } + else if (string.Equals(typeName, typeof(Episode).FullName, StringComparison.OrdinalIgnoreCase)) + { + counts.EpisodeCount = value; + } + else if (string.Equals(typeName, typeof(Movie).FullName, StringComparison.OrdinalIgnoreCase)) + { + counts.MovieCount = value; + } + else if (string.Equals(typeName, typeof(MusicAlbum).FullName, StringComparison.OrdinalIgnoreCase)) + { + counts.AlbumCount = value; + } + else if (string.Equals(typeName, typeof(MusicArtist).FullName, StringComparison.OrdinalIgnoreCase)) + { + counts.ArtistCount = value; + } + else if (string.Equals(typeName, typeof(Audio).FullName, StringComparison.OrdinalIgnoreCase)) + { + counts.SongCount = value; + } + else if (string.Equals(typeName, typeof(Game).FullName, StringComparison.OrdinalIgnoreCase)) + { + counts.GameCount = value; + } + else if (string.Equals(typeName, typeof(Trailer).FullName, StringComparison.OrdinalIgnoreCase)) + { + counts.TrailerCount = value; + } + counts.ItemCount += value; + } + + return counts; + } + + private List> GetItemValuesToSave(BaseItem item) + { + var list = new List>(); + + var hasArtist = item as IHasArtist; + if (hasArtist != null) + { + list.AddRange(hasArtist.Artists.Select(i => new Tuple(0, i))); + } + + var hasAlbumArtist = item as IHasAlbumArtist; + if (hasAlbumArtist != null) + { + list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => new Tuple(1, i))); + } + + list.AddRange(item.Genres.Select(i => new Tuple(2, i))); + list.AddRange(item.Studios.Select(i => new Tuple(3, i))); + list.AddRange(item.Tags.Select(i => new Tuple(4, i))); + list.AddRange(item.Keywords.Select(i => new Tuple(5, i))); + + return list; + } + + private void UpdateImages(Guid itemId, List images, IDbTransaction transaction) + { + if (itemId == Guid.Empty) + { + throw new ArgumentNullException("itemId"); + } + + if (images == null) + { + throw new ArgumentNullException("images"); + } + + CheckDisposed(); + + // First delete + _deleteImagesCommand.GetParameter(0).Value = itemId; + _deleteImagesCommand.Transaction = transaction; + + _deleteImagesCommand.ExecuteNonQuery(); + + var index = 0; + foreach (var image in images) + { + if (string.IsNullOrWhiteSpace(image.Path)) + { + // Invalid + continue; + } + + _saveImagesCommand.GetParameter(0).Value = itemId; + _saveImagesCommand.GetParameter(1).Value = image.Type; + _saveImagesCommand.GetParameter(2).Value = image.Path; + + if (image.DateModified == default(DateTime)) + { + _saveImagesCommand.GetParameter(3).Value = null; + } + else + { + _saveImagesCommand.GetParameter(3).Value = image.DateModified; + } + + _saveImagesCommand.GetParameter(4).Value = image.IsPlaceholder; + _saveImagesCommand.GetParameter(5).Value = index; + + _saveImagesCommand.Transaction = transaction; + + _saveImagesCommand.ExecuteNonQuery(); + index++; + } + } + + private void UpdateProviderIds(Guid itemId, Dictionary values, IDbTransaction transaction) + { + if (itemId == Guid.Empty) + { + throw new ArgumentNullException("itemId"); + } + + if (values == null) + { + throw new ArgumentNullException("values"); + } + + // Just in case there might be case-insensitive duplicates, strip them out now + var newValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var pair in values) + { + newValues[pair.Key] = pair.Value; + } + + CheckDisposed(); + + // First delete + _deleteProviderIdsCommand.GetParameter(0).Value = itemId; + _deleteProviderIdsCommand.Transaction = transaction; + + _deleteProviderIdsCommand.ExecuteNonQuery(); + + foreach (var pair in newValues) + { + if (string.IsNullOrWhiteSpace(pair.Key)) + { + continue; + } + if (string.IsNullOrWhiteSpace(pair.Value)) + { + continue; + } + + _saveProviderIdsCommand.GetParameter(0).Value = itemId; + _saveProviderIdsCommand.GetParameter(1).Value = pair.Key; + _saveProviderIdsCommand.GetParameter(2).Value = pair.Value; + _saveProviderIdsCommand.Transaction = transaction; + + _saveProviderIdsCommand.ExecuteNonQuery(); + } + } + + private void UpdateItemValues(Guid itemId, List> values, IDbTransaction transaction) + { + if (itemId == Guid.Empty) + { + throw new ArgumentNullException("itemId"); + } + + if (values == null) + { + throw new ArgumentNullException("keys"); + } + + CheckDisposed(); + + // First delete + _deleteItemValuesCommand.GetParameter(0).Value = itemId; + _deleteItemValuesCommand.Transaction = transaction; + + _deleteItemValuesCommand.ExecuteNonQuery(); + + foreach (var pair in values) + { + _saveItemValuesCommand.GetParameter(0).Value = itemId; + _saveItemValuesCommand.GetParameter(1).Value = pair.Item1; + _saveItemValuesCommand.GetParameter(2).Value = pair.Item2; + if (pair.Item2 == null) + { + _saveItemValuesCommand.GetParameter(3).Value = null; + } + else + { + _saveItemValuesCommand.GetParameter(3).Value = GetCleanValue(pair.Item2); + } + _saveItemValuesCommand.Transaction = transaction; + + _saveItemValuesCommand.ExecuteNonQuery(); + } + } + + public async Task UpdatePeople(Guid itemId, List people) + { + if (itemId == Guid.Empty) + { + throw new ArgumentNullException("itemId"); + } + + if (people == null) + { + throw new ArgumentNullException("people"); + } + + CheckDisposed(); + + var cancellationToken = CancellationToken.None; + + await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + IDbTransaction transaction = null; + + try + { + transaction = _connection.BeginTransaction(); + + // First delete + _deletePeopleCommand.GetParameter(0).Value = itemId; + _deletePeopleCommand.Transaction = transaction; + + _deletePeopleCommand.ExecuteNonQuery(); + + var listIndex = 0; + + foreach (var person in people) + { + cancellationToken.ThrowIfCancellationRequested(); + + _savePersonCommand.GetParameter(0).Value = itemId; + _savePersonCommand.GetParameter(1).Value = person.Name; + _savePersonCommand.GetParameter(2).Value = person.Role; + _savePersonCommand.GetParameter(3).Value = person.Type; + _savePersonCommand.GetParameter(4).Value = person.SortOrder; + _savePersonCommand.GetParameter(5).Value = listIndex; + + _savePersonCommand.Transaction = transaction; + + _savePersonCommand.ExecuteNonQuery(); + listIndex++; + } + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save people:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + + WriteLock.Release(); + } + } + + private PersonInfo GetPerson(IDataReader reader) + { + var item = new PersonInfo(); + + item.ItemId = reader.GetGuid(0); + item.Name = reader.GetString(1); + + if (!reader.IsDBNull(2)) + { + item.Role = reader.GetString(2); + } + + if (!reader.IsDBNull(3)) + { + item.Type = reader.GetString(3); + } + + if (!reader.IsDBNull(4)) + { + item.SortOrder = reader.GetInt32(4); + } + + return item; + } + + public IEnumerable GetMediaStreams(MediaStreamQuery query) + { + CheckDisposed(); + + if (query == null) + { + throw new ArgumentNullException("query"); + } + + var list = new List(); + + using (var cmd = _connection.CreateCommand()) + { + var cmdText = "select " + string.Join(",", _mediaStreamSaveColumns) + " from mediastreams where"; + + cmdText += " ItemId=@ItemId"; + cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = query.ItemId; + + if (query.Type.HasValue) + { + cmdText += " AND StreamType=@StreamType"; + cmd.Parameters.Add(cmd, "@StreamType", DbType.String).Value = query.Type.Value.ToString(); + } + + if (query.Index.HasValue) + { + cmdText += " AND StreamIndex=@StreamIndex"; + cmd.Parameters.Add(cmd, "@StreamIndex", DbType.Int32).Value = query.Index.Value; + } + + cmdText += " order by StreamIndex ASC"; + + cmd.CommandText = cmdText; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + list.Add(GetMediaStream(reader)); + } + } + } + + return list; + } + + public async Task SaveMediaStreams(Guid id, List streams, CancellationToken cancellationToken) + { + CheckDisposed(); + + if (id == Guid.Empty) + { + throw new ArgumentNullException("id"); + } + + if (streams == null) + { + throw new ArgumentNullException("streams"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + IDbTransaction transaction = null; + + try + { + transaction = _connection.BeginTransaction(); + + // First delete chapters + _deleteStreamsCommand.GetParameter(0).Value = id; + + _deleteStreamsCommand.Transaction = transaction; + + _deleteStreamsCommand.ExecuteNonQuery(); + + foreach (var stream in streams) + { + cancellationToken.ThrowIfCancellationRequested(); + + var index = 0; + + _saveStreamCommand.GetParameter(index++).Value = id; + _saveStreamCommand.GetParameter(index++).Value = stream.Index; + _saveStreamCommand.GetParameter(index++).Value = stream.Type.ToString(); + _saveStreamCommand.GetParameter(index++).Value = stream.Codec; + _saveStreamCommand.GetParameter(index++).Value = stream.Language; + _saveStreamCommand.GetParameter(index++).Value = stream.ChannelLayout; + _saveStreamCommand.GetParameter(index++).Value = stream.Profile; + _saveStreamCommand.GetParameter(index++).Value = stream.AspectRatio; + _saveStreamCommand.GetParameter(index++).Value = stream.Path; + + _saveStreamCommand.GetParameter(index++).Value = stream.IsInterlaced; + + _saveStreamCommand.GetParameter(index++).Value = stream.BitRate; + _saveStreamCommand.GetParameter(index++).Value = stream.Channels; + _saveStreamCommand.GetParameter(index++).Value = stream.SampleRate; + + _saveStreamCommand.GetParameter(index++).Value = stream.IsDefault; + _saveStreamCommand.GetParameter(index++).Value = stream.IsForced; + _saveStreamCommand.GetParameter(index++).Value = stream.IsExternal; + + _saveStreamCommand.GetParameter(index++).Value = stream.Width; + _saveStreamCommand.GetParameter(index++).Value = stream.Height; + _saveStreamCommand.GetParameter(index++).Value = stream.AverageFrameRate; + _saveStreamCommand.GetParameter(index++).Value = stream.RealFrameRate; + _saveStreamCommand.GetParameter(index++).Value = stream.Level; + _saveStreamCommand.GetParameter(index++).Value = stream.PixelFormat; + _saveStreamCommand.GetParameter(index++).Value = stream.BitDepth; + _saveStreamCommand.GetParameter(index++).Value = stream.IsAnamorphic; + _saveStreamCommand.GetParameter(index++).Value = stream.RefFrames; + + _saveStreamCommand.GetParameter(index++).Value = stream.CodecTag; + _saveStreamCommand.GetParameter(index++).Value = stream.Comment; + _saveStreamCommand.GetParameter(index++).Value = stream.NalLengthSize; + _saveStreamCommand.GetParameter(index++).Value = stream.IsAVC; + _saveStreamCommand.GetParameter(index++).Value = stream.Title; + + _saveStreamCommand.GetParameter(index++).Value = stream.TimeBase; + _saveStreamCommand.GetParameter(index++).Value = stream.CodecTimeBase; + + _saveStreamCommand.Transaction = transaction; + _saveStreamCommand.ExecuteNonQuery(); + } + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save media streams:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + + WriteLock.Release(); + } + } + + /// + /// Gets the chapter. + /// + /// The reader. + /// ChapterInfo. + private MediaStream GetMediaStream(IDataReader reader) + { + var item = new MediaStream + { + Index = reader.GetInt32(1) + }; + + item.Type = (MediaStreamType)Enum.Parse(typeof(MediaStreamType), reader.GetString(2), true); + + if (!reader.IsDBNull(3)) + { + item.Codec = reader.GetString(3); + } + + if (!reader.IsDBNull(4)) + { + item.Language = reader.GetString(4); + } + + if (!reader.IsDBNull(5)) + { + item.ChannelLayout = reader.GetString(5); + } + + if (!reader.IsDBNull(6)) + { + item.Profile = reader.GetString(6); + } + + if (!reader.IsDBNull(7)) + { + item.AspectRatio = reader.GetString(7); + } + + if (!reader.IsDBNull(8)) + { + item.Path = reader.GetString(8); + } + + item.IsInterlaced = reader.GetBoolean(9); + + if (!reader.IsDBNull(10)) + { + item.BitRate = reader.GetInt32(10); + } + + if (!reader.IsDBNull(11)) + { + item.Channels = reader.GetInt32(11); + } + + if (!reader.IsDBNull(12)) + { + item.SampleRate = reader.GetInt32(12); + } + + item.IsDefault = reader.GetBoolean(13); + item.IsForced = reader.GetBoolean(14); + item.IsExternal = reader.GetBoolean(15); + + if (!reader.IsDBNull(16)) + { + item.Width = reader.GetInt32(16); + } + + if (!reader.IsDBNull(17)) + { + item.Height = reader.GetInt32(17); + } + + if (!reader.IsDBNull(18)) + { + item.AverageFrameRate = reader.GetFloat(18); + } + + if (!reader.IsDBNull(19)) + { + item.RealFrameRate = reader.GetFloat(19); + } + + if (!reader.IsDBNull(20)) + { + item.Level = reader.GetFloat(20); + } + + if (!reader.IsDBNull(21)) + { + item.PixelFormat = reader.GetString(21); + } + + if (!reader.IsDBNull(22)) + { + item.BitDepth = reader.GetInt32(22); + } + + if (!reader.IsDBNull(23)) + { + item.IsAnamorphic = reader.GetBoolean(23); + } + + if (!reader.IsDBNull(24)) + { + item.RefFrames = reader.GetInt32(24); + } + + if (!reader.IsDBNull(25)) + { + item.CodecTag = reader.GetString(25); + } + + if (!reader.IsDBNull(26)) + { + item.Comment = reader.GetString(26); + } + + if (!reader.IsDBNull(27)) + { + item.NalLengthSize = reader.GetString(27); + } + + if (!reader.IsDBNull(28)) + { + item.IsAVC = reader.GetBoolean(28); + } + + if (!reader.IsDBNull(29)) + { + item.Title = reader.GetString(29); + } + + if (!reader.IsDBNull(30)) + { + item.TimeBase = reader.GetString(30); + } + + if (!reader.IsDBNull(31)) + { + item.CodecTimeBase = reader.GetString(31); + } + + return item; + } + + } +} \ No newline at end of file diff --git a/Emby.Server.Core/Data/SqliteUserDataRepository.cs b/Emby.Server.Core/Data/SqliteUserDataRepository.cs new file mode 100644 index 0000000000..2c883e0431 --- /dev/null +++ b/Emby.Server.Core/Data/SqliteUserDataRepository.cs @@ -0,0 +1,453 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Logging; + +namespace Emby.Server.Core.Data +{ + public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository + { + private IDbConnection _connection; + + public SqliteUserDataRepository(ILogManager logManager, IApplicationPaths appPaths, IDbConnector connector) : base(logManager, connector) + { + DbFilePath = Path.Combine(appPaths.DataPath, "userdata_v2.db"); + } + + protected override bool EnableConnectionPooling + { + get { return false; } + } + + /// + /// Gets the name of the repository + /// + /// The name. + public string Name + { + get + { + return "SQLite"; + } + } + + protected override async Task CreateConnection(bool isReadOnly = false) + { + var connection = await DbConnector.Connect(DbFilePath, false, false, 10000).ConfigureAwait(false); + + connection.RunQueries(new[] + { + "pragma temp_store = memory" + + }, Logger); + + return connection; + } + + /// + /// Opens the connection to the database + /// + /// Task. + public async Task Initialize(IDbConnection connection, SemaphoreSlim writeLock) + { + WriteLock.Dispose(); + WriteLock = writeLock; + _connection = connection; + + string[] queries = { + + "create table if not exists UserDataDb.userdata (key nvarchar, userId GUID, rating float null, played bit, playCount int, isFavorite bit, playbackPositionTicks bigint, lastPlayedDate datetime null)", + + "drop index if exists UserDataDb.idx_userdata", + "drop index if exists UserDataDb.idx_userdata1", + "drop index if exists UserDataDb.idx_userdata2", + "drop index if exists UserDataDb.userdataindex1", + + "create unique index if not exists UserDataDb.userdataindex on userdata (key, userId)", + "create index if not exists UserDataDb.userdataindex2 on userdata (key, userId, played)", + "create index if not exists UserDataDb.userdataindex3 on userdata (key, userId, playbackPositionTicks)", + "create index if not exists UserDataDb.userdataindex4 on userdata (key, userId, isFavorite)", + + //pragmas + "pragma temp_store = memory", + + "pragma shrink_memory" + }; + + _connection.RunQueries(queries, Logger); + + _connection.AddColumn(Logger, "userdata", "AudioStreamIndex", "int"); + _connection.AddColumn(Logger, "userdata", "SubtitleStreamIndex", "int"); + } + + /// + /// Saves the user data. + /// + /// The user id. + /// The key. + /// The user data. + /// The cancellation token. + /// Task. + /// userData + /// or + /// cancellationToken + /// or + /// userId + /// or + /// userDataId + public Task SaveUserData(Guid userId, string key, UserItemData userData, CancellationToken cancellationToken) + { + if (userData == null) + { + throw new ArgumentNullException("userData"); + } + if (userId == Guid.Empty) + { + throw new ArgumentNullException("userId"); + } + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentNullException("key"); + } + + return PersistUserData(userId, key, userData, cancellationToken); + } + + public Task SaveAllUserData(Guid userId, IEnumerable userData, CancellationToken cancellationToken) + { + if (userData == null) + { + throw new ArgumentNullException("userData"); + } + if (userId == Guid.Empty) + { + throw new ArgumentNullException("userId"); + } + + return PersistAllUserData(userId, userData, cancellationToken); + } + + /// + /// Persists the user data. + /// + /// The user id. + /// The key. + /// The user data. + /// The cancellation token. + /// Task. + public async Task PersistUserData(Guid userId, string key, UserItemData userData, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + IDbTransaction transaction = null; + + try + { + transaction = _connection.BeginTransaction(); + + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "replace into userdata (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"; + + cmd.Parameters.Add(cmd, "@key", DbType.String).Value = key; + cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId; + cmd.Parameters.Add(cmd, "@rating", DbType.Double).Value = userData.Rating; + cmd.Parameters.Add(cmd, "@played", DbType.Boolean).Value = userData.Played; + cmd.Parameters.Add(cmd, "@playCount", DbType.Int32).Value = userData.PlayCount; + cmd.Parameters.Add(cmd, "@isFavorite", DbType.Boolean).Value = userData.IsFavorite; + cmd.Parameters.Add(cmd, "@playbackPositionTicks", DbType.Int64).Value = userData.PlaybackPositionTicks; + cmd.Parameters.Add(cmd, "@lastPlayedDate", DbType.DateTime).Value = userData.LastPlayedDate; + cmd.Parameters.Add(cmd, "@AudioStreamIndex", DbType.Int32).Value = userData.AudioStreamIndex; + cmd.Parameters.Add(cmd, "@SubtitleStreamIndex", DbType.Int32).Value = userData.SubtitleStreamIndex; + + cmd.Transaction = transaction; + + cmd.ExecuteNonQuery(); + } + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save user data:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + + WriteLock.Release(); + } + } + + /// + /// Persist all user data for the specified user + /// + /// + /// + /// + /// + private async Task PersistAllUserData(Guid userId, IEnumerable userData, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + IDbTransaction transaction = null; + + try + { + transaction = _connection.BeginTransaction(); + + foreach (var userItemData in userData) + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "replace into userdata (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"; + + cmd.Parameters.Add(cmd, "@key", DbType.String).Value = userItemData.Key; + cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId; + cmd.Parameters.Add(cmd, "@rating", DbType.Double).Value = userItemData.Rating; + cmd.Parameters.Add(cmd, "@played", DbType.Boolean).Value = userItemData.Played; + cmd.Parameters.Add(cmd, "@playCount", DbType.Int32).Value = userItemData.PlayCount; + cmd.Parameters.Add(cmd, "@isFavorite", DbType.Boolean).Value = userItemData.IsFavorite; + cmd.Parameters.Add(cmd, "@playbackPositionTicks", DbType.Int64).Value = userItemData.PlaybackPositionTicks; + cmd.Parameters.Add(cmd, "@lastPlayedDate", DbType.DateTime).Value = userItemData.LastPlayedDate; + cmd.Parameters.Add(cmd, "@AudioStreamIndex", DbType.Int32).Value = userItemData.AudioStreamIndex; + cmd.Parameters.Add(cmd, "@SubtitleStreamIndex", DbType.Int32).Value = userItemData.SubtitleStreamIndex; + + cmd.Transaction = transaction; + + cmd.ExecuteNonQuery(); + } + + cancellationToken.ThrowIfCancellationRequested(); + } + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save user data:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + + WriteLock.Release(); + } + } + + /// + /// Gets the user data. + /// + /// The user id. + /// The key. + /// Task{UserItemData}. + /// + /// userId + /// or + /// key + /// + public UserItemData GetUserData(Guid userId, string key) + { + if (userId == Guid.Empty) + { + throw new ArgumentNullException("userId"); + } + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentNullException("key"); + } + + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from userdata where key = @key and userId=@userId"; + + cmd.Parameters.Add(cmd, "@key", DbType.String).Value = key; + cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) + { + if (reader.Read()) + { + return ReadRow(reader); + } + } + + return null; + } + } + + public UserItemData GetUserData(Guid userId, List keys) + { + if (userId == Guid.Empty) + { + throw new ArgumentNullException("userId"); + } + if (keys == null) + { + throw new ArgumentNullException("keys"); + } + + using (var cmd = _connection.CreateCommand()) + { + var index = 0; + var userdataKeys = new List(); + var builder = new StringBuilder(); + foreach (var key in keys) + { + var paramName = "@Key" + index; + userdataKeys.Add("Key =" + paramName); + cmd.Parameters.Add(cmd, paramName, DbType.String).Value = key; + builder.Append(" WHEN Key=" + paramName + " THEN " + index); + index++; + break; + } + + var keyText = string.Join(" OR ", userdataKeys.ToArray()); + + cmd.CommandText = "select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from userdata where userId=@userId AND (" + keyText + ") "; + + cmd.CommandText += " ORDER BY (Case " + builder + " Else " + keys.Count.ToString(CultureInfo.InvariantCulture) + " End )"; + cmd.CommandText += " LIMIT 1"; + + cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) + { + if (reader.Read()) + { + return ReadRow(reader); + } + } + + return null; + } + } + + /// + /// Return all user-data associated with the given user + /// + /// + /// + public IEnumerable GetAllUserData(Guid userId) + { + if (userId == Guid.Empty) + { + throw new ArgumentNullException("userId"); + } + + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from userdata where userId=@userId"; + + cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + yield return ReadRow(reader); + } + } + } + } + + /// + /// Read a row from the specified reader into the provided userData object + /// + /// + private UserItemData ReadRow(IDataReader reader) + { + var userData = new UserItemData(); + + userData.Key = reader.GetString(0); + userData.UserId = reader.GetGuid(1); + + if (!reader.IsDBNull(2)) + { + userData.Rating = reader.GetDouble(2); + } + + userData.Played = reader.GetBoolean(3); + userData.PlayCount = reader.GetInt32(4); + userData.IsFavorite = reader.GetBoolean(5); + userData.PlaybackPositionTicks = reader.GetInt64(6); + + if (!reader.IsDBNull(7)) + { + userData.LastPlayedDate = reader.GetDateTime(7).ToUniversalTime(); + } + + if (!reader.IsDBNull(8)) + { + userData.AudioStreamIndex = reader.GetInt32(8); + } + + if (!reader.IsDBNull(9)) + { + userData.SubtitleStreamIndex = reader.GetInt32(9); + } + + return userData; + } + + protected override void Dispose(bool dispose) + { + // handled by library database + } + + protected override void CloseConnection() + { + // handled by library database + } + } +} \ No newline at end of file diff --git a/Emby.Server.Core/Data/SqliteUserRepository.cs b/Emby.Server.Core/Data/SqliteUserRepository.cs new file mode 100644 index 0000000000..54851fffbf --- /dev/null +++ b/Emby.Server.Core/Data/SqliteUserRepository.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; + +namespace Emby.Server.Core.Data +{ + /// + /// Class SQLiteUserRepository + /// + public class SqliteUserRepository : BaseSqliteRepository, IUserRepository + { + private readonly IJsonSerializer _jsonSerializer; + private readonly IMemoryStreamFactory _memoryStreamProvider; + + public SqliteUserRepository(ILogManager logManager, IServerApplicationPaths appPaths, IJsonSerializer jsonSerializer, IDbConnector dbConnector, IMemoryStreamFactory memoryStreamProvider) : base(logManager, dbConnector) + { + _jsonSerializer = jsonSerializer; + _memoryStreamProvider = memoryStreamProvider; + + DbFilePath = Path.Combine(appPaths.DataPath, "users.db"); + } + + /// + /// Gets the name of the repository + /// + /// The name. + public string Name + { + get + { + return "SQLite"; + } + } + + /// + /// Opens the connection to the database + /// + /// Task. + public async Task Initialize() + { + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + string[] queries = { + + "create table if not exists users (guid GUID primary key, data BLOB)", + "create index if not exists idx_users on users(guid)", + "create table if not exists schema_version (table_name primary key, version)", + + "pragma shrink_memory" + }; + + connection.RunQueries(queries, Logger); + } + } + + /// + /// Save a user in the repo + /// + /// The user. + /// The cancellation token. + /// Task. + /// user + public async Task SaveUser(User user, CancellationToken cancellationToken) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + var serialized = _jsonSerializer.SerializeToBytes(user, _memoryStreamProvider); + + cancellationToken.ThrowIfCancellationRequested(); + + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + IDbTransaction transaction = null; + + try + { + transaction = connection.BeginTransaction(); + + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "replace into users (guid, data) values (@1, @2)"; + cmd.Parameters.Add(cmd, "@1", DbType.Guid).Value = user.Id; + cmd.Parameters.Add(cmd, "@2", DbType.Binary).Value = serialized; + + cmd.Transaction = transaction; + + cmd.ExecuteNonQuery(); + } + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save user:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } + } + } + + /// + /// Retrieve all users from the database + /// + /// IEnumerable{User}. + public IEnumerable RetrieveAllUsers() + { + var list = new List(); + + using (var connection = CreateConnection(true).Result) + { + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "select guid,data from users"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + var id = reader.GetGuid(0); + + using (var stream = reader.GetMemoryStream(1, _memoryStreamProvider)) + { + var user = _jsonSerializer.DeserializeFromStream(stream); + user.Id = id; + list.Add(user); + } + } + } + } + } + + return list; + } + + /// + /// Deletes the user. + /// + /// The user. + /// The cancellation token. + /// Task. + /// user + public async Task DeleteUser(User user, CancellationToken cancellationToken) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + IDbTransaction transaction = null; + + try + { + transaction = connection.BeginTransaction(); + + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "delete from users where guid=@guid"; + + cmd.Parameters.Add(cmd, "@guid", DbType.Guid).Value = user.Id; + + cmd.Transaction = transaction; + + cmd.ExecuteNonQuery(); + } + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to delete user:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } + } + } + } +} \ No newline at end of file diff --git a/Emby.Server.Core/Data/TypeMapper.cs b/Emby.Server.Core/Data/TypeMapper.cs new file mode 100644 index 0000000000..f8eb5dd2d8 --- /dev/null +++ b/Emby.Server.Core/Data/TypeMapper.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; + +namespace Emby.Server.Core.Data +{ + /// + /// Class TypeMapper + /// + public class TypeMapper + { + /// + /// This holds all the types in the running assemblies so that we can de-serialize properly when we don't have strong types + /// + private readonly ConcurrentDictionary _typeMap = new ConcurrentDictionary(); + + /// + /// Gets the type. + /// + /// Name of the type. + /// Type. + /// + public Type GetType(string typeName) + { + if (string.IsNullOrEmpty(typeName)) + { + throw new ArgumentNullException(); + } + + return _typeMap.GetOrAdd(typeName, LookupType); + } + + /// + /// Lookups the type. + /// + /// Name of the type. + /// Type. + private Type LookupType(string typeName) + { + return AppDomain + .CurrentDomain + .GetAssemblies() + .Select(a => a.GetType(typeName, false)) + .FirstOrDefault(t => t != null); + } + } +} diff --git a/Emby.Server.Core/Devices/DeviceRepository.cs b/Emby.Server.Core/Devices/DeviceRepository.cs new file mode 100644 index 0000000000..63aa7b67a0 --- /dev/null +++ b/Emby.Server.Core/Devices/DeviceRepository.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Model.Devices; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Session; + +namespace Emby.Server.Core.Devices +{ + public class DeviceRepository : IDeviceRepository + { + private readonly object _syncLock = new object(); + + private readonly IApplicationPaths _appPaths; + private readonly IJsonSerializer _json; + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + + private Dictionary _devices; + + public DeviceRepository(IApplicationPaths appPaths, IJsonSerializer json, ILogger logger, IFileSystem fileSystem) + { + _appPaths = appPaths; + _json = json; + _logger = logger; + _fileSystem = fileSystem; + } + + private string GetDevicesPath() + { + return Path.Combine(_appPaths.DataPath, "devices"); + } + + private string GetDevicePath(string id) + { + return Path.Combine(GetDevicesPath(), id.GetMD5().ToString("N")); + } + + public Task SaveDevice(DeviceInfo device) + { + var path = Path.Combine(GetDevicePath(device.Id), "device.json"); + _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); + + lock (_syncLock) + { + _json.SerializeToFile(device, path); + _devices[device.Id] = device; + } + return Task.FromResult(true); + } + + public Task SaveCapabilities(string reportedId, ClientCapabilities capabilities) + { + var device = GetDevice(reportedId); + + if (device == null) + { + throw new ArgumentException("No device has been registed with id " + reportedId); + } + + device.Capabilities = capabilities; + SaveDevice(device); + + return Task.FromResult(true); + } + + public ClientCapabilities GetCapabilities(string reportedId) + { + var device = GetDevice(reportedId); + + return device == null ? null : device.Capabilities; + } + + public DeviceInfo GetDevice(string id) + { + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentNullException("id"); + } + + return GetDevices() + .FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)); + } + + public IEnumerable GetDevices() + { + lock (_syncLock) + { + if (_devices == null) + { + _devices = new Dictionary(StringComparer.OrdinalIgnoreCase); + + var devices = LoadDevices().ToList(); + foreach (var device in devices) + { + _devices[device.Id] = device; + } + } + return _devices.Values.ToList(); + } + } + + private IEnumerable LoadDevices() + { + var path = GetDevicesPath(); + + try + { + return _fileSystem + .GetFilePaths(path, true) + .Where(i => string.Equals(Path.GetFileName(i), "device.json", StringComparison.OrdinalIgnoreCase)) + .ToList() + .Select(i => + { + try + { + return _json.DeserializeFromFile(i); + } + catch (Exception ex) + { + _logger.ErrorException("Error reading {0}", ex, i); + return null; + } + }) + .Where(i => i != null); + } + catch (IOException) + { + return new List(); + } + } + + public Task DeleteDevice(string id) + { + var path = GetDevicePath(id); + + lock (_syncLock) + { + try + { + _fileSystem.DeleteDirectory(path, true); + } + catch (DirectoryNotFoundException) + { + } + + _devices = null; + } + + return Task.FromResult(true); + } + + public ContentUploadHistory GetCameraUploadHistory(string deviceId) + { + var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json"); + + lock (_syncLock) + { + try + { + return _json.DeserializeFromFile(path); + } + catch (IOException) + { + return new ContentUploadHistory + { + DeviceId = deviceId + }; + } + } + } + + public void AddCameraUpload(string deviceId, LocalFileInfo file) + { + var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json"); + _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); + + lock (_syncLock) + { + ContentUploadHistory history; + + try + { + history = _json.DeserializeFromFile(path); + } + catch (IOException) + { + history = new ContentUploadHistory + { + DeviceId = deviceId + }; + } + + history.DeviceId = deviceId; + history.FilesUploaded.Add(file); + + _json.SerializeToFile(history, path); + } + } + } +} diff --git a/Emby.Server.Core/Emby.Server.Core.xproj b/Emby.Server.Core/Emby.Server.Core.xproj index 19bc7babcb..94de75bff9 100644 --- a/Emby.Server.Core/Emby.Server.Core.xproj +++ b/Emby.Server.Core/Emby.Server.Core.xproj @@ -16,6 +16,7 @@ 2.0 + diff --git a/Emby.Server.Core/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Core/EntryPoints/ExternalPortForwarding.cs new file mode 100644 index 0000000000..c1a19a71fa --- /dev/null +++ b/Emby.Server.Core/EntryPoints/ExternalPortForwarding.cs @@ -0,0 +1,292 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Threading; +using Mono.Nat; + +namespace Emby.Server.Core.EntryPoints +{ + public class ExternalPortForwarding : IServerEntryPoint + { + private readonly IServerApplicationHost _appHost; + private readonly ILogger _logger; + private readonly IHttpClient _httpClient; + private readonly IServerConfigurationManager _config; + private readonly IDeviceDiscovery _deviceDiscovery; + + private ITimer _timer; + private bool _isStarted; + private readonly ITimerFactory _timerFactory; + + public ExternalPortForwarding(ILogManager logmanager, IServerApplicationHost appHost, IServerConfigurationManager config, IDeviceDiscovery deviceDiscovery, IHttpClient httpClient, ITimerFactory timerFactory) + { + _logger = logmanager.GetLogger("PortMapper"); + _appHost = appHost; + _config = config; + _deviceDiscovery = deviceDiscovery; + _httpClient = httpClient; + _timerFactory = timerFactory; + } + + private string _lastConfigIdentifier; + private string GetConfigIdentifier() + { + var values = new List(); + var config = _config.Configuration; + + values.Add(config.EnableUPnP.ToString()); + values.Add(config.PublicPort.ToString(CultureInfo.InvariantCulture)); + values.Add(_appHost.HttpPort.ToString(CultureInfo.InvariantCulture)); + values.Add(_appHost.HttpsPort.ToString(CultureInfo.InvariantCulture)); + values.Add(config.EnableHttps.ToString()); + values.Add(_appHost.EnableHttps.ToString()); + + return string.Join("|", values.ToArray()); + } + + void _config_ConfigurationUpdated(object sender, EventArgs e) + { + if (!string.Equals(_lastConfigIdentifier, GetConfigIdentifier(), StringComparison.OrdinalIgnoreCase)) + { + if (_isStarted) + { + DisposeNat(); + } + + Run(); + } + } + + public void Run() + { + NatUtility.Logger = _logger; + NatUtility.HttpClient = _httpClient; + + if (_config.Configuration.EnableUPnP) + { + Start(); + } + + _config.ConfigurationUpdated -= _config_ConfigurationUpdated; + _config.ConfigurationUpdated += _config_ConfigurationUpdated; + } + + private void Start() + { + _logger.Debug("Starting NAT discovery"); + NatUtility.EnabledProtocols = new List + { + NatProtocol.Pmp + }; + NatUtility.DeviceFound += NatUtility_DeviceFound; + + // Mono.Nat does never rise this event. The event is there however it is useless. + // You could remove it with no risk. + NatUtility.DeviceLost += NatUtility_DeviceLost; + + + NatUtility.StartDiscovery(); + + _timer = _timerFactory.Create(ClearCreatedRules, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); + + _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; + + _lastConfigIdentifier = GetConfigIdentifier(); + + _isStarted = true; + } + + private async void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs e) + { + var info = e.Argument; + + string usn; + if (!info.Headers.TryGetValue("USN", out usn)) usn = string.Empty; + + string nt; + if (!info.Headers.TryGetValue("NT", out nt)) nt = string.Empty; + + // Filter device type + if (usn.IndexOf("WANIPConnection:", StringComparison.OrdinalIgnoreCase) == -1 && + nt.IndexOf("WANIPConnection:", StringComparison.OrdinalIgnoreCase) == -1 && + usn.IndexOf("WANPPPConnection:", StringComparison.OrdinalIgnoreCase) == -1 && + nt.IndexOf("WANPPPConnection:", StringComparison.OrdinalIgnoreCase) == -1) + { + return; + } + + var identifier = string.IsNullOrWhiteSpace(usn) ? nt : usn; + + if (info.Location == null) + { + return; + } + + lock (_usnsHandled) + { + if (_usnsHandled.Contains(identifier)) + { + return; + } + _usnsHandled.Add(identifier); + } + + _logger.Debug("Found NAT device: " + identifier); + + IPAddress address; + if (IPAddress.TryParse(info.Location.Host, out address)) + { + // The Handle method doesn't need the port + var endpoint = new IPEndPoint(address, info.Location.Port); + + IPAddress localAddress = null; + + try + { + var localAddressString = await _appHost.GetLocalApiUrl().ConfigureAwait(false); + + Uri uri; + if (Uri.TryCreate(localAddressString, UriKind.Absolute, out uri)) + { + localAddressString = uri.Host; + + if (!IPAddress.TryParse(localAddressString, out localAddress)) + { + return; + } + } + } + catch (Exception ex) + { + return; + } + + _logger.Debug("Calling Nat.Handle on " + identifier); + NatUtility.Handle(localAddress, info, endpoint, NatProtocol.Upnp); + } + } + + private void ClearCreatedRules(object state) + { + _createdRules = new List(); + lock (_usnsHandled) + { + _usnsHandled.Clear(); + } + } + + void NatUtility_DeviceFound(object sender, DeviceEventArgs e) + { + try + { + var device = e.Device; + _logger.Debug("NAT device found: {0}", device.LocalAddress.ToString()); + + CreateRules(device); + } + catch + { + // I think it could be a good idea to log the exception because + // you are using permanent portmapping here (never expire) and that means that next time + // CreatePortMap is invoked it can fails with a 718-ConflictInMappingEntry or not. That depends + // on the router's upnp implementation (specs says it should fail however some routers don't do it) + // It also can fail with others like 727-ExternalPortOnlySupportsWildcard, 728-NoPortMapsAvailable + // and those errors (upnp errors) could be useful for diagnosting. + + // Commenting out because users are reporting problems out of our control + //_logger.ErrorException("Error creating port forwarding rules", ex); + } + } + + private List _createdRules = new List(); + private List _usnsHandled = new List(); + private void CreateRules(INatDevice device) + { + // On some systems the device discovered event seems to fire repeatedly + // This check will help ensure we're not trying to port map the same device over and over + + var address = device.LocalAddress.ToString(); + + if (!_createdRules.Contains(address)) + { + _createdRules.Add(address); + + CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort); + CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort); + } + } + + private async void CreatePortMap(INatDevice device, int privatePort, int publicPort) + { + _logger.Debug("Creating port map on port {0}", privatePort); + + try + { + await device.CreatePortMap(new Mapping(Protocol.Tcp, privatePort, publicPort) + { + Description = _appHost.Name + }).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error creating port map", ex); + } + } + + // As I said before, this method will be never invoked. You can remove it. + void NatUtility_DeviceLost(object sender, DeviceEventArgs e) + { + var device = e.Device; + _logger.Debug("NAT device lost: {0}", device.LocalAddress.ToString()); + } + + public void Dispose() + { + DisposeNat(); + } + + private void DisposeNat() + { + _logger.Debug("Stopping NAT discovery"); + + if (_timer != null) + { + _timer.Dispose(); + _timer = null; + } + + _deviceDiscovery.DeviceDiscovered -= _deviceDiscovery_DeviceDiscovered; + + try + { + // This is not a significant improvement + NatUtility.StopDiscovery(); + NatUtility.DeviceFound -= NatUtility_DeviceFound; + NatUtility.DeviceLost -= NatUtility_DeviceLost; + } + // Statements in try-block will no fail because StopDiscovery is a one-line + // method that was no chances to fail. + // public static void StopDiscovery () + // { + // searching.Reset(); + // } + // IMO you could remove the catch-block + catch (Exception ex) + { + _logger.ErrorException("Error stopping NAT Discovery", ex); + } + finally + { + _isStarted = false; + } + } + } +} \ No newline at end of file diff --git a/Emby.Server.Core/FFMpeg/FFMpegInfo.cs b/Emby.Server.Core/FFMpeg/FFMpegInfo.cs new file mode 100644 index 0000000000..fc197cba32 --- /dev/null +++ b/Emby.Server.Core/FFMpeg/FFMpegInfo.cs @@ -0,0 +1,24 @@ +namespace Emby.Server.Core.FFMpeg +{ + /// + /// Class FFMpegInfo + /// + public class FFMpegInfo + { + /// + /// Gets or sets the path. + /// + /// The path. + public string EncoderPath { get; set; } + /// + /// Gets or sets the probe path. + /// + /// The probe path. + public string ProbePath { get; set; } + /// + /// Gets or sets the version. + /// + /// The version. + public string Version { get; set; } + } +} \ No newline at end of file diff --git a/Emby.Server.Core/FFMpeg/FFMpegInstallInfo.cs b/Emby.Server.Core/FFMpeg/FFMpegInstallInfo.cs new file mode 100644 index 0000000000..f1eaeb8894 --- /dev/null +++ b/Emby.Server.Core/FFMpeg/FFMpegInstallInfo.cs @@ -0,0 +1,20 @@ + +namespace Emby.Server.Core.FFMpeg +{ + public class FFMpegInstallInfo + { + public string Version { get; set; } + public string FFMpegFilename { get; set; } + public string FFProbeFilename { get; set; } + public string ArchiveType { get; set; } + public string[] DownloadUrls { get; set; } + + public FFMpegInstallInfo() + { + DownloadUrls = new string[] { }; + Version = "Path"; + FFMpegFilename = "ffmpeg"; + FFProbeFilename = "ffprobe"; + } + } +} \ No newline at end of file diff --git a/Emby.Server.Core/Migrations/DbMigration.cs b/Emby.Server.Core/Migrations/DbMigration.cs new file mode 100644 index 0000000000..17e0860938 --- /dev/null +++ b/Emby.Server.Core/Migrations/DbMigration.cs @@ -0,0 +1,63 @@ +using System.Threading.Tasks; +using Emby.Server.Implementations.Persistence; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.Tasks; +using Emby.Server.Core.Data; + +namespace Emby.Server.Core.Migrations +{ + public class DbMigration : IVersionMigration + { + private readonly IServerConfigurationManager _config; + private readonly ITaskManager _taskManager; + + public DbMigration(IServerConfigurationManager config, ITaskManager taskManager) + { + _config = config; + _taskManager = taskManager; + } + + public async Task Run() + { + // If a forced migration is required, do that now + if (_config.Configuration.MigrationVersion < CleanDatabaseScheduledTask.MigrationVersion) + { + if (!_config.Configuration.IsStartupWizardCompleted) + { + _config.Configuration.MigrationVersion = CleanDatabaseScheduledTask.MigrationVersion; + _config.SaveConfiguration(); + return; + } + + _taskManager.SuspendTriggers = true; + CleanDatabaseScheduledTask.EnableUnavailableMessage = true; + + Task.Run(async () => + { + await Task.Delay(1000).ConfigureAwait(false); + + _taskManager.Execute(); + }); + + return; + } + + if (_config.Configuration.SchemaVersion < SqliteItemRepository.LatestSchemaVersion) + { + if (!_config.Configuration.IsStartupWizardCompleted) + { + _config.Configuration.SchemaVersion = SqliteItemRepository.LatestSchemaVersion; + _config.SaveConfiguration(); + return; + } + + Task.Run(async () => + { + await Task.Delay(1000).ConfigureAwait(false); + + _taskManager.Execute(); + }); + } + } + } +} diff --git a/Emby.Server.Core/Migrations/IVersionMigration.cs b/Emby.Server.Core/Migrations/IVersionMigration.cs new file mode 100644 index 0000000000..0190e289a4 --- /dev/null +++ b/Emby.Server.Core/Migrations/IVersionMigration.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Emby.Server.Core.Migrations +{ + public interface IVersionMigration + { + Task Run(); + } +} diff --git a/Emby.Server.Core/Migrations/UpdateLevelMigration.cs b/Emby.Server.Core/Migrations/UpdateLevelMigration.cs new file mode 100644 index 0000000000..bbedd7b63c --- /dev/null +++ b/Emby.Server.Core/Migrations/UpdateLevelMigration.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Emby.Common.Implementations.Updates; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Updates; + +namespace Emby.Server.Core.Migrations +{ + public class UpdateLevelMigration : IVersionMigration + { + private readonly IServerConfigurationManager _config; + private readonly IServerApplicationHost _appHost; + private readonly IHttpClient _httpClient; + private readonly IJsonSerializer _jsonSerializer; + private readonly string _releaseAssetFilename; + private readonly ILogger _logger; + + public UpdateLevelMigration(IServerConfigurationManager config, IServerApplicationHost appHost, IHttpClient httpClient, IJsonSerializer jsonSerializer, string releaseAssetFilename, ILogger logger) + { + _config = config; + _appHost = appHost; + _httpClient = httpClient; + _jsonSerializer = jsonSerializer; + _releaseAssetFilename = releaseAssetFilename; + _logger = logger; + } + + public async Task Run() + { + var lastVersion = _config.Configuration.LastVersion; + var currentVersion = _appHost.ApplicationVersion; + + if (string.Equals(lastVersion, currentVersion.ToString(), StringComparison.OrdinalIgnoreCase)) + { + return; + } + + try + { + var updateLevel = _config.Configuration.SystemUpdateLevel; + + await CheckVersion(currentVersion, updateLevel, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error in update migration", ex); + } + } + + private async Task CheckVersion(Version currentVersion, PackageVersionClass currentUpdateLevel, CancellationToken cancellationToken) + { + var releases = await new GithubUpdater(_httpClient, _jsonSerializer) + .GetLatestReleases("MediaBrowser", "Emby", _releaseAssetFilename, cancellationToken).ConfigureAwait(false); + + var newUpdateLevel = GetNewUpdateLevel(currentVersion, currentUpdateLevel, releases); + + if (newUpdateLevel != currentUpdateLevel) + { + _config.Configuration.SystemUpdateLevel = newUpdateLevel; + _config.SaveConfiguration(); + } + } + + private PackageVersionClass GetNewUpdateLevel(Version currentVersion, PackageVersionClass currentUpdateLevel, List releases) + { + var newUpdateLevel = currentUpdateLevel; + + // If the current version is later than current stable, set the update level to beta + if (releases.Count >= 1) + { + var release = releases[0]; + var version = ParseVersion(release.tag_name); + if (version != null) + { + if (currentVersion > version) + { + newUpdateLevel = PackageVersionClass.Beta; + } + else + { + return PackageVersionClass.Release; + } + } + } + + // If the current version is later than current beta, set the update level to dev + if (releases.Count >= 2) + { + var release = releases[1]; + var version = ParseVersion(release.tag_name); + if (version != null) + { + if (currentVersion > version) + { + newUpdateLevel = PackageVersionClass.Dev; + } + else + { + return PackageVersionClass.Beta; + } + } + } + + return newUpdateLevel; + } + + private Version ParseVersion(string versionString) + { + if (!string.IsNullOrWhiteSpace(versionString)) + { + var parts = versionString.Split('.'); + if (parts.Length == 3) + { + versionString += ".0"; + } + } + + Version version; + Version.TryParse(versionString, out version); + + return version; + } + } +} diff --git a/Emby.Server.Core/Notifications/SqliteNotificationsRepository.cs b/Emby.Server.Core/Notifications/SqliteNotificationsRepository.cs new file mode 100644 index 0000000000..8a7fc92701 --- /dev/null +++ b/Emby.Server.Core/Notifications/SqliteNotificationsRepository.cs @@ -0,0 +1,470 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Emby.Server.Core.Data; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Notifications; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Notifications; + +namespace Emby.Server.Core.Notifications +{ + public class SqliteNotificationsRepository : BaseSqliteRepository, INotificationsRepository + { + public SqliteNotificationsRepository(ILogManager logManager, IServerApplicationPaths appPaths, IDbConnector dbConnector) : base(logManager, dbConnector) + { + DbFilePath = Path.Combine(appPaths.DataPath, "notifications.db"); + } + + public event EventHandler NotificationAdded; + public event EventHandler NotificationsMarkedRead; + ////public event EventHandler NotificationUpdated; + + public async Task Initialize() + { + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + string[] queries = { + + "create table if not exists Notifications (Id GUID NOT NULL, UserId GUID NOT NULL, Date DATETIME NOT NULL, Name TEXT NOT NULL, Description TEXT, Url TEXT, Level TEXT NOT NULL, IsRead BOOLEAN NOT NULL, Category TEXT NOT NULL, RelatedId TEXT, PRIMARY KEY (Id, UserId))", + "create index if not exists idx_Notifications1 on Notifications(Id)", + "create index if not exists idx_Notifications2 on Notifications(UserId)" + }; + + connection.RunQueries(queries, Logger); + } + } + + /// + /// Gets the notifications. + /// + /// The query. + /// NotificationResult. + public NotificationResult GetNotifications(NotificationQuery query) + { + var result = new NotificationResult(); + + using (var connection = CreateConnection(true).Result) + { + using (var cmd = connection.CreateCommand()) + { + var clauses = new List(); + + if (query.IsRead.HasValue) + { + clauses.Add("IsRead=@IsRead"); + cmd.Parameters.Add(cmd, "@IsRead", DbType.Boolean).Value = query.IsRead.Value; + } + + clauses.Add("UserId=@UserId"); + cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = new Guid(query.UserId); + + var whereClause = " where " + string.Join(" And ", clauses.ToArray()); + + cmd.CommandText = string.Format("select count(Id) from Notifications{0};select Id,UserId,Date,Name,Description,Url,Level,IsRead,Category,RelatedId from Notifications{0} order by IsRead asc, Date desc", whereClause); + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) + { + if (reader.Read()) + { + result.TotalRecordCount = reader.GetInt32(0); + } + + if (reader.NextResult()) + { + var notifications = GetNotifications(reader); + + if (query.StartIndex.HasValue) + { + notifications = notifications.Skip(query.StartIndex.Value); + } + + if (query.Limit.HasValue) + { + notifications = notifications.Take(query.Limit.Value); + } + + result.Notifications = notifications.ToArray(); + } + } + + return result; + } + } + } + + public NotificationsSummary GetNotificationsSummary(string userId) + { + var result = new NotificationsSummary(); + + using (var connection = CreateConnection(true).Result) + { + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "select Level from Notifications where UserId=@UserId and IsRead=@IsRead"; + + cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = new Guid(userId); + cmd.Parameters.Add(cmd, "@IsRead", DbType.Boolean).Value = false; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) + { + var levels = new List(); + + while (reader.Read()) + { + levels.Add(GetLevel(reader, 0)); + } + + result.UnreadCount = levels.Count; + + if (levels.Count > 0) + { + result.MaxUnreadNotificationLevel = levels.Max(); + } + } + + return result; + } + } + } + + /// + /// Gets the notifications. + /// + /// The reader. + /// IEnumerable{Notification}. + private IEnumerable GetNotifications(IDataReader reader) + { + var list = new List(); + + while (reader.Read()) + { + list.Add(GetNotification(reader)); + } + + return list; + } + + private Notification GetNotification(IDataReader reader) + { + var notification = new Notification + { + Id = reader.GetGuid(0).ToString("N"), + UserId = reader.GetGuid(1).ToString("N"), + Date = reader.GetDateTime(2).ToUniversalTime(), + Name = reader.GetString(3) + }; + + if (!reader.IsDBNull(4)) + { + notification.Description = reader.GetString(4); + } + + if (!reader.IsDBNull(5)) + { + notification.Url = reader.GetString(5); + } + + notification.Level = GetLevel(reader, 6); + notification.IsRead = reader.GetBoolean(7); + + return notification; + } + + /// + /// Gets the level. + /// + /// The reader. + /// The index. + /// NotificationLevel. + private NotificationLevel GetLevel(IDataReader reader, int index) + { + NotificationLevel level; + + var val = reader.GetString(index); + + Enum.TryParse(val, true, out level); + + return level; + } + + /// + /// Adds the notification. + /// + /// The notification. + /// The cancellation token. + /// Task. + public async Task AddNotification(Notification notification, CancellationToken cancellationToken) + { + await ReplaceNotification(notification, cancellationToken).ConfigureAwait(false); + + if (NotificationAdded != null) + { + try + { + NotificationAdded(this, new NotificationUpdateEventArgs + { + Notification = notification + }); + } + catch (Exception ex) + { + Logger.ErrorException("Error in NotificationAdded event handler", ex); + } + } + } + + /// + /// Replaces the notification. + /// + /// The notification. + /// The cancellation token. + /// Task. + private async Task ReplaceNotification(Notification notification, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(notification.Id)) + { + notification.Id = Guid.NewGuid().ToString("N"); + } + if (string.IsNullOrEmpty(notification.UserId)) + { + throw new ArgumentException("The notification must have a user id"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + using (var replaceNotificationCommand = connection.CreateCommand()) + { + replaceNotificationCommand.CommandText = "replace into Notifications (Id, UserId, Date, Name, Description, Url, Level, IsRead, Category, RelatedId) values (@Id, @UserId, @Date, @Name, @Description, @Url, @Level, @IsRead, @Category, @RelatedId)"; + + replaceNotificationCommand.Parameters.Add(replaceNotificationCommand, "@Id"); + replaceNotificationCommand.Parameters.Add(replaceNotificationCommand, "@UserId"); + replaceNotificationCommand.Parameters.Add(replaceNotificationCommand, "@Date"); + replaceNotificationCommand.Parameters.Add(replaceNotificationCommand, "@Name"); + replaceNotificationCommand.Parameters.Add(replaceNotificationCommand, "@Description"); + replaceNotificationCommand.Parameters.Add(replaceNotificationCommand, "@Url"); + replaceNotificationCommand.Parameters.Add(replaceNotificationCommand, "@Level"); + replaceNotificationCommand.Parameters.Add(replaceNotificationCommand, "@IsRead"); + replaceNotificationCommand.Parameters.Add(replaceNotificationCommand, "@Category"); + replaceNotificationCommand.Parameters.Add(replaceNotificationCommand, "@RelatedId"); + + IDbTransaction transaction = null; + + try + { + transaction = connection.BeginTransaction(); + + replaceNotificationCommand.GetParameter(0).Value = new Guid(notification.Id); + replaceNotificationCommand.GetParameter(1).Value = new Guid(notification.UserId); + replaceNotificationCommand.GetParameter(2).Value = notification.Date.ToUniversalTime(); + replaceNotificationCommand.GetParameter(3).Value = notification.Name; + replaceNotificationCommand.GetParameter(4).Value = notification.Description; + replaceNotificationCommand.GetParameter(5).Value = notification.Url; + replaceNotificationCommand.GetParameter(6).Value = notification.Level.ToString(); + replaceNotificationCommand.GetParameter(7).Value = notification.IsRead; + replaceNotificationCommand.GetParameter(8).Value = string.Empty; + replaceNotificationCommand.GetParameter(9).Value = string.Empty; + + replaceNotificationCommand.Transaction = transaction; + + replaceNotificationCommand.ExecuteNonQuery(); + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save notification:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } + } + } + } + + /// + /// Marks the read. + /// + /// The notification id list. + /// The user id. + /// if set to true [is read]. + /// The cancellation token. + /// Task. + public async Task MarkRead(IEnumerable notificationIdList, string userId, bool isRead, CancellationToken cancellationToken) + { + var list = notificationIdList.ToList(); + var idArray = list.Select(i => new Guid(i)).ToArray(); + + await MarkReadInternal(idArray, userId, isRead, cancellationToken).ConfigureAwait(false); + + if (NotificationsMarkedRead != null) + { + try + { + NotificationsMarkedRead(this, new NotificationReadEventArgs + { + IdList = list.ToArray(), + IsRead = isRead, + UserId = userId + }); + } + catch (Exception ex) + { + Logger.ErrorException("Error in NotificationsMarkedRead event handler", ex); + } + } + } + + public async Task MarkAllRead(string userId, bool isRead, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + using (var markAllReadCommand = connection.CreateCommand()) + { + markAllReadCommand.CommandText = "update Notifications set IsRead=@IsRead where UserId=@UserId"; + + markAllReadCommand.Parameters.Add(markAllReadCommand, "@UserId"); + markAllReadCommand.Parameters.Add(markAllReadCommand, "@IsRead"); + + IDbTransaction transaction = null; + + try + { + cancellationToken.ThrowIfCancellationRequested(); + + transaction = connection.BeginTransaction(); + + markAllReadCommand.GetParameter(0).Value = new Guid(userId); + markAllReadCommand.GetParameter(1).Value = isRead; + + markAllReadCommand.ExecuteNonQuery(); + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save notification:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } + } + } + } + + private async Task MarkReadInternal(IEnumerable notificationIdList, string userId, bool isRead, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + using (var markReadCommand = connection.CreateCommand()) + { + markReadCommand.CommandText = "update Notifications set IsRead=@IsRead where Id=@Id and UserId=@UserId"; + + markReadCommand.Parameters.Add(markReadCommand, "@UserId"); + markReadCommand.Parameters.Add(markReadCommand, "@IsRead"); + markReadCommand.Parameters.Add(markReadCommand, "@Id"); + + IDbTransaction transaction = null; + + try + { + cancellationToken.ThrowIfCancellationRequested(); + + transaction = connection.BeginTransaction(); + + markReadCommand.GetParameter(0).Value = new Guid(userId); + markReadCommand.GetParameter(1).Value = isRead; + + foreach (var id in notificationIdList) + { + markReadCommand.GetParameter(2).Value = id; + + markReadCommand.Transaction = transaction; + + markReadCommand.ExecuteNonQuery(); + } + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save notification:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } + } + } + } + } +} diff --git a/Emby.Server.Core/Social/SharingRepository.cs b/Emby.Server.Core/Social/SharingRepository.cs new file mode 100644 index 0000000000..5503b7ab3b --- /dev/null +++ b/Emby.Server.Core/Social/SharingRepository.cs @@ -0,0 +1,158 @@ +using System; +using System.Data; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Emby.Server.Core.Data; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Social; + +namespace Emby.Server.Core.Social +{ + public class SharingRepository : BaseSqliteRepository, ISharingRepository + { + public SharingRepository(ILogManager logManager, IApplicationPaths appPaths, IDbConnector dbConnector) + : base(logManager, dbConnector) + { + DbFilePath = Path.Combine(appPaths.DataPath, "shares.db"); + } + + /// + /// Opens the connection to the database + /// + /// Task. + public async Task Initialize() + { + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + string[] queries = { + + "create table if not exists Shares (Id GUID, ItemId TEXT, UserId TEXT, ExpirationDate DateTime, PRIMARY KEY (Id))", + "create index if not exists idx_Shares on Shares(Id)", + + "pragma shrink_memory" + }; + + connection.RunQueries(queries, Logger); + } + } + + public async Task CreateShare(SocialShareInfo info) + { + if (info == null) + { + throw new ArgumentNullException("info"); + } + if (string.IsNullOrWhiteSpace(info.Id)) + { + throw new ArgumentNullException("info.Id"); + } + + var cancellationToken = CancellationToken.None; + + cancellationToken.ThrowIfCancellationRequested(); + + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + using (var saveShareCommand = connection.CreateCommand()) + { + saveShareCommand.CommandText = "replace into Shares (Id, ItemId, UserId, ExpirationDate) values (@Id, @ItemId, @UserId, @ExpirationDate)"; + + saveShareCommand.Parameters.Add(saveShareCommand, "@Id"); + saveShareCommand.Parameters.Add(saveShareCommand, "@ItemId"); + saveShareCommand.Parameters.Add(saveShareCommand, "@UserId"); + saveShareCommand.Parameters.Add(saveShareCommand, "@ExpirationDate"); + + IDbTransaction transaction = null; + + try + { + transaction = connection.BeginTransaction(); + + saveShareCommand.GetParameter(0).Value = new Guid(info.Id); + saveShareCommand.GetParameter(1).Value = info.ItemId; + saveShareCommand.GetParameter(2).Value = info.UserId; + saveShareCommand.GetParameter(3).Value = info.ExpirationDate; + + saveShareCommand.Transaction = transaction; + + saveShareCommand.ExecuteNonQuery(); + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save share:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } + } + } + } + + public SocialShareInfo GetShareInfo(string id) + { + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentNullException("id"); + } + + using (var connection = CreateConnection(true).Result) + { + var cmd = connection.CreateCommand(); + cmd.CommandText = "select Id, ItemId, UserId, ExpirationDate from Shares where id = @id"; + + cmd.Parameters.Add(cmd, "@id", DbType.Guid).Value = new Guid(id); + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) + { + if (reader.Read()) + { + return GetSocialShareInfo(reader); + } + } + + return null; + } + } + + private SocialShareInfo GetSocialShareInfo(IDataReader reader) + { + var info = new SocialShareInfo(); + + info.Id = reader.GetGuid(0).ToString("N"); + info.ItemId = reader.GetString(1); + info.UserId = reader.GetString(2); + info.ExpirationDate = reader.GetDateTime(3).ToUniversalTime(); + + return info; + } + + public async Task DeleteShare(string id) + { + + } + } +} diff --git a/Emby.Server.Core/Sync/SyncRepository.cs b/Emby.Server.Core/Sync/SyncRepository.cs new file mode 100644 index 0000000000..bfcf76767c --- /dev/null +++ b/Emby.Server.Core/Sync/SyncRepository.cs @@ -0,0 +1,976 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Emby.Server.Core.Data; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Sync; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Sync; + +namespace Emby.Server.Core.Sync +{ + public class SyncRepository : BaseSqliteRepository, ISyncRepository + { + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + private readonly IJsonSerializer _json; + + public SyncRepository(ILogManager logManager, IJsonSerializer json, IServerApplicationPaths appPaths, IDbConnector connector) + : base(logManager, connector) + { + _json = json; + DbFilePath = Path.Combine(appPaths.DataPath, "sync14.db"); + } + + private class SyncSummary + { + public Dictionary Items { get; set; } + + public SyncSummary() + { + Items = new Dictionary(); + } + } + + + public async Task Initialize() + { + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + string[] queries = { + + "create table if not exists SyncJobs (Id GUID PRIMARY KEY, TargetId TEXT NOT NULL, Name TEXT NOT NULL, Profile TEXT, Quality TEXT, Bitrate INT, Status TEXT NOT NULL, Progress FLOAT, UserId TEXT NOT NULL, ItemIds TEXT NOT NULL, Category TEXT, ParentId TEXT, UnwatchedOnly BIT, ItemLimit INT, SyncNewContent BIT, DateCreated DateTime, DateLastModified DateTime, ItemCount int)", + + "create table if not exists SyncJobItems (Id GUID PRIMARY KEY, ItemId TEXT, ItemName TEXT, MediaSourceId TEXT, JobId TEXT, TemporaryPath TEXT, OutputPath TEXT, Status TEXT, TargetId TEXT, DateCreated DateTime, Progress FLOAT, AdditionalFiles TEXT, MediaSource TEXT, IsMarkedForRemoval BIT, JobItemIndex INT, ItemDateModifiedTicks BIGINT)", + + "drop index if exists idx_SyncJobItems2", + "drop index if exists idx_SyncJobItems3", + "drop index if exists idx_SyncJobs1", + "drop index if exists idx_SyncJobs", + "drop index if exists idx_SyncJobItems1", + "create index if not exists idx_SyncJobItems4 on SyncJobItems(TargetId,ItemId,Status,Progress,DateCreated)", + "create index if not exists idx_SyncJobItems5 on SyncJobItems(TargetId,Status,ItemId,Progress)", + + "create index if not exists idx_SyncJobs2 on SyncJobs(TargetId,Status,ItemIds,Progress)", + + "pragma shrink_memory" + }; + + connection.RunQueries(queries, Logger); + + connection.AddColumn(Logger, "SyncJobs", "Profile", "TEXT"); + connection.AddColumn(Logger, "SyncJobs", "Bitrate", "INT"); + connection.AddColumn(Logger, "SyncJobItems", "ItemDateModifiedTicks", "BIGINT"); + } + } + + private const string BaseJobSelectText = "select Id, TargetId, Name, Profile, Quality, Bitrate, Status, Progress, UserId, ItemIds, Category, ParentId, UnwatchedOnly, ItemLimit, SyncNewContent, DateCreated, DateLastModified, ItemCount from SyncJobs"; + private const string BaseJobItemSelectText = "select Id, ItemId, ItemName, MediaSourceId, JobId, TemporaryPath, OutputPath, Status, TargetId, DateCreated, Progress, AdditionalFiles, MediaSource, IsMarkedForRemoval, JobItemIndex, ItemDateModifiedTicks from SyncJobItems"; + + public SyncJob GetJob(string id) + { + if (string.IsNullOrEmpty(id)) + { + throw new ArgumentNullException("id"); + } + + CheckDisposed(); + + var guid = new Guid(id); + + if (guid == Guid.Empty) + { + throw new ArgumentNullException("id"); + } + + using (var connection = CreateConnection(true).Result) + { + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = BaseJobSelectText + " where Id=@Id"; + + cmd.Parameters.Add(cmd, "@Id", DbType.Guid).Value = guid; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) + { + if (reader.Read()) + { + return GetJob(reader); + } + } + } + + return null; + } + } + + private SyncJob GetJob(IDataReader reader) + { + var info = new SyncJob + { + Id = reader.GetGuid(0).ToString("N"), + TargetId = reader.GetString(1), + Name = reader.GetString(2) + }; + + if (!reader.IsDBNull(3)) + { + info.Profile = reader.GetString(3); + } + + if (!reader.IsDBNull(4)) + { + info.Quality = reader.GetString(4); + } + + if (!reader.IsDBNull(5)) + { + info.Bitrate = reader.GetInt32(5); + } + + if (!reader.IsDBNull(6)) + { + info.Status = (SyncJobStatus)Enum.Parse(typeof(SyncJobStatus), reader.GetString(6), true); + } + + if (!reader.IsDBNull(7)) + { + info.Progress = reader.GetDouble(7); + } + + if (!reader.IsDBNull(8)) + { + info.UserId = reader.GetString(8); + } + + if (!reader.IsDBNull(9)) + { + info.RequestedItemIds = reader.GetString(9).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); + } + + if (!reader.IsDBNull(10)) + { + info.Category = (SyncCategory)Enum.Parse(typeof(SyncCategory), reader.GetString(10), true); + } + + if (!reader.IsDBNull(11)) + { + info.ParentId = reader.GetString(11); + } + + if (!reader.IsDBNull(12)) + { + info.UnwatchedOnly = reader.GetBoolean(12); + } + + if (!reader.IsDBNull(13)) + { + info.ItemLimit = reader.GetInt32(13); + } + + info.SyncNewContent = reader.GetBoolean(14); + + info.DateCreated = reader.GetDateTime(15).ToUniversalTime(); + info.DateLastModified = reader.GetDateTime(16).ToUniversalTime(); + info.ItemCount = reader.GetInt32(17); + + return info; + } + + public Task Create(SyncJob job) + { + return InsertOrUpdate(job, true); + } + + public Task Update(SyncJob job) + { + return InsertOrUpdate(job, false); + } + + private async Task InsertOrUpdate(SyncJob job, bool insert) + { + if (job == null) + { + throw new ArgumentNullException("job"); + } + + CheckDisposed(); + + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + using (var cmd = connection.CreateCommand()) + { + if (insert) + { + cmd.CommandText = "insert into SyncJobs (Id, TargetId, Name, Profile, Quality, Bitrate, Status, Progress, UserId, ItemIds, Category, ParentId, UnwatchedOnly, ItemLimit, SyncNewContent, DateCreated, DateLastModified, ItemCount) values (@Id, @TargetId, @Name, @Profile, @Quality, @Bitrate, @Status, @Progress, @UserId, @ItemIds, @Category, @ParentId, @UnwatchedOnly, @ItemLimit, @SyncNewContent, @DateCreated, @DateLastModified, @ItemCount)"; + + cmd.Parameters.Add(cmd, "@Id"); + cmd.Parameters.Add(cmd, "@TargetId"); + cmd.Parameters.Add(cmd, "@Name"); + cmd.Parameters.Add(cmd, "@Profile"); + cmd.Parameters.Add(cmd, "@Quality"); + cmd.Parameters.Add(cmd, "@Bitrate"); + cmd.Parameters.Add(cmd, "@Status"); + cmd.Parameters.Add(cmd, "@Progress"); + cmd.Parameters.Add(cmd, "@UserId"); + cmd.Parameters.Add(cmd, "@ItemIds"); + cmd.Parameters.Add(cmd, "@Category"); + cmd.Parameters.Add(cmd, "@ParentId"); + cmd.Parameters.Add(cmd, "@UnwatchedOnly"); + cmd.Parameters.Add(cmd, "@ItemLimit"); + cmd.Parameters.Add(cmd, "@SyncNewContent"); + cmd.Parameters.Add(cmd, "@DateCreated"); + cmd.Parameters.Add(cmd, "@DateLastModified"); + cmd.Parameters.Add(cmd, "@ItemCount"); + } + else + { + cmd.CommandText = "update SyncJobs set TargetId=@TargetId,Name=@Name,Profile=@Profile,Quality=@Quality,Bitrate=@Bitrate,Status=@Status,Progress=@Progress,UserId=@UserId,ItemIds=@ItemIds,Category=@Category,ParentId=@ParentId,UnwatchedOnly=@UnwatchedOnly,ItemLimit=@ItemLimit,SyncNewContent=@SyncNewContent,DateCreated=@DateCreated,DateLastModified=@DateLastModified,ItemCount=@ItemCount where Id=@Id"; + + cmd.Parameters.Add(cmd, "@Id"); + cmd.Parameters.Add(cmd, "@TargetId"); + cmd.Parameters.Add(cmd, "@Name"); + cmd.Parameters.Add(cmd, "@Profile"); + cmd.Parameters.Add(cmd, "@Quality"); + cmd.Parameters.Add(cmd, "@Bitrate"); + cmd.Parameters.Add(cmd, "@Status"); + cmd.Parameters.Add(cmd, "@Progress"); + cmd.Parameters.Add(cmd, "@UserId"); + cmd.Parameters.Add(cmd, "@ItemIds"); + cmd.Parameters.Add(cmd, "@Category"); + cmd.Parameters.Add(cmd, "@ParentId"); + cmd.Parameters.Add(cmd, "@UnwatchedOnly"); + cmd.Parameters.Add(cmd, "@ItemLimit"); + cmd.Parameters.Add(cmd, "@SyncNewContent"); + cmd.Parameters.Add(cmd, "@DateCreated"); + cmd.Parameters.Add(cmd, "@DateLastModified"); + cmd.Parameters.Add(cmd, "@ItemCount"); + } + + IDbTransaction transaction = null; + + try + { + transaction = connection.BeginTransaction(); + + var index = 0; + + cmd.GetParameter(index++).Value = new Guid(job.Id); + cmd.GetParameter(index++).Value = job.TargetId; + cmd.GetParameter(index++).Value = job.Name; + cmd.GetParameter(index++).Value = job.Profile; + cmd.GetParameter(index++).Value = job.Quality; + cmd.GetParameter(index++).Value = job.Bitrate; + cmd.GetParameter(index++).Value = job.Status.ToString(); + cmd.GetParameter(index++).Value = job.Progress; + cmd.GetParameter(index++).Value = job.UserId; + cmd.GetParameter(index++).Value = string.Join(",", job.RequestedItemIds.ToArray()); + cmd.GetParameter(index++).Value = job.Category; + cmd.GetParameter(index++).Value = job.ParentId; + cmd.GetParameter(index++).Value = job.UnwatchedOnly; + cmd.GetParameter(index++).Value = job.ItemLimit; + cmd.GetParameter(index++).Value = job.SyncNewContent; + cmd.GetParameter(index++).Value = job.DateCreated; + cmd.GetParameter(index++).Value = job.DateLastModified; + cmd.GetParameter(index++).Value = job.ItemCount; + + cmd.Transaction = transaction; + + cmd.ExecuteNonQuery(); + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save record:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } + } + } + } + + public async Task DeleteJob(string id) + { + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentNullException("id"); + } + + CheckDisposed(); + + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + using (var deleteJobCommand = connection.CreateCommand()) + { + using (var deleteJobItemsCommand = connection.CreateCommand()) + { + IDbTransaction transaction = null; + + try + { + // _deleteJobCommand + deleteJobCommand.CommandText = "delete from SyncJobs where Id=@Id"; + deleteJobCommand.Parameters.Add(deleteJobCommand, "@Id"); + + transaction = connection.BeginTransaction(); + + deleteJobCommand.GetParameter(0).Value = new Guid(id); + deleteJobCommand.Transaction = transaction; + deleteJobCommand.ExecuteNonQuery(); + + // _deleteJobItemsCommand + deleteJobItemsCommand.CommandText = "delete from SyncJobItems where JobId=@JobId"; + deleteJobItemsCommand.Parameters.Add(deleteJobItemsCommand, "@JobId"); + + deleteJobItemsCommand.GetParameter(0).Value = id; + deleteJobItemsCommand.Transaction = transaction; + deleteJobItemsCommand.ExecuteNonQuery(); + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save record:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } + } + } + } + } + + public QueryResult GetJobs(SyncJobQuery query) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + CheckDisposed(); + + using (var connection = CreateConnection(true).Result) + { + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = BaseJobSelectText; + + var whereClauses = new List(); + + if (query.Statuses.Length > 0) + { + var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray()); + + whereClauses.Add(string.Format("Status in ({0})", statuses)); + } + if (!string.IsNullOrWhiteSpace(query.TargetId)) + { + whereClauses.Add("TargetId=@TargetId"); + cmd.Parameters.Add(cmd, "@TargetId", DbType.String).Value = query.TargetId; + } + if (!string.IsNullOrWhiteSpace(query.ExcludeTargetIds)) + { + var excludeIds = (query.ExcludeTargetIds ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + if (excludeIds.Length == 1) + { + whereClauses.Add("TargetId<>@ExcludeTargetId"); + cmd.Parameters.Add(cmd, "@ExcludeTargetId", DbType.String).Value = excludeIds[0]; + } + else if (excludeIds.Length > 1) + { + whereClauses.Add("TargetId<>@ExcludeTargetId"); + cmd.Parameters.Add(cmd, "@ExcludeTargetId", DbType.String).Value = excludeIds[0]; + } + } + if (!string.IsNullOrWhiteSpace(query.UserId)) + { + whereClauses.Add("UserId=@UserId"); + cmd.Parameters.Add(cmd, "@UserId", DbType.String).Value = query.UserId; + } + if (query.SyncNewContent.HasValue) + { + whereClauses.Add("SyncNewContent=@SyncNewContent"); + cmd.Parameters.Add(cmd, "@SyncNewContent", DbType.Boolean).Value = query.SyncNewContent.Value; + } + + cmd.CommandText += " mainTable"; + + var whereTextWithoutPaging = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + var startIndex = query.StartIndex ?? 0; + if (startIndex > 0) + { + whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM SyncJobs ORDER BY (Select Max(DateLastModified) from SyncJobs where TargetId=mainTable.TargetId) DESC, DateLastModified DESC LIMIT {0})", + startIndex.ToString(_usCulture))); + } + + if (whereClauses.Count > 0) + { + cmd.CommandText += " where " + string.Join(" AND ", whereClauses.ToArray()); + } + + cmd.CommandText += " ORDER BY (Select Max(DateLastModified) from SyncJobs where TargetId=mainTable.TargetId) DESC, DateLastModified DESC"; + + if (query.Limit.HasValue) + { + cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); + } + + cmd.CommandText += "; select count (Id) from SyncJobs" + whereTextWithoutPaging; + + var list = new List(); + var count = 0; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) + { + while (reader.Read()) + { + list.Add(GetJob(reader)); + } + + if (reader.NextResult() && reader.Read()) + { + count = reader.GetInt32(0); + } + } + + return new QueryResult() + { + Items = list.ToArray(), + TotalRecordCount = count + }; + } + } + } + + public SyncJobItem GetJobItem(string id) + { + if (string.IsNullOrEmpty(id)) + { + throw new ArgumentNullException("id"); + } + + CheckDisposed(); + + var guid = new Guid(id); + + using (var connection = CreateConnection(true).Result) + { + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = BaseJobItemSelectText + " where Id=@Id"; + + cmd.Parameters.Add(cmd, "@Id", DbType.Guid).Value = guid; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) + { + if (reader.Read()) + { + return GetJobItem(reader); + } + } + } + + return null; + } + } + + private QueryResult GetJobItemReader(SyncJobItemQuery query, string baseSelectText, Func itemFactory) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + using (var connection = CreateConnection(true).Result) + { + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = baseSelectText; + + var whereClauses = new List(); + + if (!string.IsNullOrWhiteSpace(query.JobId)) + { + whereClauses.Add("JobId=@JobId"); + cmd.Parameters.Add(cmd, "@JobId", DbType.String).Value = query.JobId; + } + if (!string.IsNullOrWhiteSpace(query.ItemId)) + { + whereClauses.Add("ItemId=@ItemId"); + cmd.Parameters.Add(cmd, "@ItemId", DbType.String).Value = query.ItemId; + } + if (!string.IsNullOrWhiteSpace(query.TargetId)) + { + whereClauses.Add("TargetId=@TargetId"); + cmd.Parameters.Add(cmd, "@TargetId", DbType.String).Value = query.TargetId; + } + + if (query.Statuses.Length > 0) + { + var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray()); + + whereClauses.Add(string.Format("Status in ({0})", statuses)); + } + + var whereTextWithoutPaging = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + var startIndex = query.StartIndex ?? 0; + if (startIndex > 0) + { + whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM SyncJobItems ORDER BY JobItemIndex, DateCreated LIMIT {0})", + startIndex.ToString(_usCulture))); + } + + if (whereClauses.Count > 0) + { + cmd.CommandText += " where " + string.Join(" AND ", whereClauses.ToArray()); + } + + cmd.CommandText += " ORDER BY JobItemIndex, DateCreated"; + + if (query.Limit.HasValue) + { + cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); + } + + cmd.CommandText += "; select count (Id) from SyncJobItems" + whereTextWithoutPaging; + + var list = new List(); + var count = 0; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) + { + while (reader.Read()) + { + list.Add(itemFactory(reader)); + } + + if (reader.NextResult() && reader.Read()) + { + count = reader.GetInt32(0); + } + } + + return new QueryResult() + { + Items = list.ToArray(), + TotalRecordCount = count + }; + } + } + } + + public Dictionary GetSyncedItemProgresses(SyncJobItemQuery query) + { + var result = new Dictionary(); + + var now = DateTime.UtcNow; + + using (var connection = CreateConnection(true).Result) + { + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "select ItemId,Status,Progress from SyncJobItems"; + + var whereClauses = new List(); + + if (!string.IsNullOrWhiteSpace(query.TargetId)) + { + whereClauses.Add("TargetId=@TargetId"); + cmd.Parameters.Add(cmd, "@TargetId", DbType.String).Value = query.TargetId; + } + + if (query.Statuses.Length > 0) + { + var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray()); + + whereClauses.Add(string.Format("Status in ({0})", statuses)); + } + + if (whereClauses.Count > 0) + { + cmd.CommandText += " where " + string.Join(" AND ", whereClauses.ToArray()); + } + + cmd.CommandText += ";" + cmd.CommandText + .Replace("select ItemId,Status,Progress from SyncJobItems", "select ItemIds,Status,Progress from SyncJobs") + .Replace("'Synced'", "'Completed','CompletedWithError'"); + + //Logger.Debug(cmd.CommandText); + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) + { + LogQueryTime("GetSyncedItemProgresses", cmd, now); + + while (reader.Read()) + { + AddStatusResult(reader, result, false); + } + + if (reader.NextResult()) + { + while (reader.Read()) + { + AddStatusResult(reader, result, true); + } + } + } + } + } + + return result; + } + + private void LogQueryTime(string methodName, IDbCommand cmd, DateTime startDate) + { + var elapsed = (DateTime.UtcNow - startDate).TotalMilliseconds; + + var slowThreshold = 1000; + +#if DEBUG + slowThreshold = 50; +#endif + + if (elapsed >= slowThreshold) + { + Logger.Debug("{2} query time (slow): {0}ms. Query: {1}", + Convert.ToInt32(elapsed), + cmd.CommandText, + methodName); + } + else + { + //Logger.Debug("{2} query time: {0}ms. Query: {1}", + // Convert.ToInt32(elapsed), + // cmd.CommandText, + // methodName); + } + } + + private void AddStatusResult(IDataReader reader, Dictionary result, bool multipleIds) + { + if (reader.IsDBNull(0)) + { + return; + } + + var itemIds = new List(); + + var ids = reader.GetString(0); + + if (multipleIds) + { + itemIds = ids.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); + } + else + { + itemIds.Add(ids); + } + + if (!reader.IsDBNull(1)) + { + SyncJobItemStatus status; + var statusString = reader.GetString(1); + if (string.Equals(statusString, "Completed", StringComparison.OrdinalIgnoreCase) || + string.Equals(statusString, "CompletedWithError", StringComparison.OrdinalIgnoreCase)) + { + status = SyncJobItemStatus.Synced; + } + else + { + status = (SyncJobItemStatus)Enum.Parse(typeof(SyncJobItemStatus), statusString, true); + } + + if (status == SyncJobItemStatus.Synced) + { + foreach (var itemId in itemIds) + { + result[itemId] = new SyncedItemProgress + { + Status = SyncJobItemStatus.Synced + }; + } + } + else + { + double progress = reader.IsDBNull(2) ? 0.0 : reader.GetDouble(2); + + foreach (var itemId in itemIds) + { + SyncedItemProgress currentStatus; + if (!result.TryGetValue(itemId, out currentStatus) || (currentStatus.Status != SyncJobItemStatus.Synced && progress >= currentStatus.Progress)) + { + result[itemId] = new SyncedItemProgress + { + Status = status, + Progress = progress + }; + } + } + } + } + } + + public QueryResult GetJobItems(SyncJobItemQuery query) + { + return GetJobItemReader(query, BaseJobItemSelectText, GetJobItem); + } + + public Task Create(SyncJobItem jobItem) + { + return InsertOrUpdate(jobItem, true); + } + + public Task Update(SyncJobItem jobItem) + { + return InsertOrUpdate(jobItem, false); + } + + private async Task InsertOrUpdate(SyncJobItem jobItem, bool insert) + { + if (jobItem == null) + { + throw new ArgumentNullException("jobItem"); + } + + CheckDisposed(); + + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + using (var cmd = connection.CreateCommand()) + { + if (insert) + { + cmd.CommandText = "insert into SyncJobItems (Id, ItemId, ItemName, MediaSourceId, JobId, TemporaryPath, OutputPath, Status, TargetId, DateCreated, Progress, AdditionalFiles, MediaSource, IsMarkedForRemoval, JobItemIndex, ItemDateModifiedTicks) values (@Id, @ItemId, @ItemName, @MediaSourceId, @JobId, @TemporaryPath, @OutputPath, @Status, @TargetId, @DateCreated, @Progress, @AdditionalFiles, @MediaSource, @IsMarkedForRemoval, @JobItemIndex, @ItemDateModifiedTicks)"; + + cmd.Parameters.Add(cmd, "@Id"); + cmd.Parameters.Add(cmd, "@ItemId"); + cmd.Parameters.Add(cmd, "@ItemName"); + cmd.Parameters.Add(cmd, "@MediaSourceId"); + cmd.Parameters.Add(cmd, "@JobId"); + cmd.Parameters.Add(cmd, "@TemporaryPath"); + cmd.Parameters.Add(cmd, "@OutputPath"); + cmd.Parameters.Add(cmd, "@Status"); + cmd.Parameters.Add(cmd, "@TargetId"); + cmd.Parameters.Add(cmd, "@DateCreated"); + cmd.Parameters.Add(cmd, "@Progress"); + cmd.Parameters.Add(cmd, "@AdditionalFiles"); + cmd.Parameters.Add(cmd, "@MediaSource"); + cmd.Parameters.Add(cmd, "@IsMarkedForRemoval"); + cmd.Parameters.Add(cmd, "@JobItemIndex"); + cmd.Parameters.Add(cmd, "@ItemDateModifiedTicks"); + } + else + { + // cmd + cmd.CommandText = "update SyncJobItems set ItemId=@ItemId,ItemName=@ItemName,MediaSourceId=@MediaSourceId,JobId=@JobId,TemporaryPath=@TemporaryPath,OutputPath=@OutputPath,Status=@Status,TargetId=@TargetId,DateCreated=@DateCreated,Progress=@Progress,AdditionalFiles=@AdditionalFiles,MediaSource=@MediaSource,IsMarkedForRemoval=@IsMarkedForRemoval,JobItemIndex=@JobItemIndex,ItemDateModifiedTicks=@ItemDateModifiedTicks where Id=@Id"; + + cmd.Parameters.Add(cmd, "@Id"); + cmd.Parameters.Add(cmd, "@ItemId"); + cmd.Parameters.Add(cmd, "@ItemName"); + cmd.Parameters.Add(cmd, "@MediaSourceId"); + cmd.Parameters.Add(cmd, "@JobId"); + cmd.Parameters.Add(cmd, "@TemporaryPath"); + cmd.Parameters.Add(cmd, "@OutputPath"); + cmd.Parameters.Add(cmd, "@Status"); + cmd.Parameters.Add(cmd, "@TargetId"); + cmd.Parameters.Add(cmd, "@DateCreated"); + cmd.Parameters.Add(cmd, "@Progress"); + cmd.Parameters.Add(cmd, "@AdditionalFiles"); + cmd.Parameters.Add(cmd, "@MediaSource"); + cmd.Parameters.Add(cmd, "@IsMarkedForRemoval"); + cmd.Parameters.Add(cmd, "@JobItemIndex"); + cmd.Parameters.Add(cmd, "@ItemDateModifiedTicks"); + } + + IDbTransaction transaction = null; + + try + { + transaction = connection.BeginTransaction(); + + var index = 0; + + cmd.GetParameter(index++).Value = new Guid(jobItem.Id); + cmd.GetParameter(index++).Value = jobItem.ItemId; + cmd.GetParameter(index++).Value = jobItem.ItemName; + cmd.GetParameter(index++).Value = jobItem.MediaSourceId; + cmd.GetParameter(index++).Value = jobItem.JobId; + cmd.GetParameter(index++).Value = jobItem.TemporaryPath; + cmd.GetParameter(index++).Value = jobItem.OutputPath; + cmd.GetParameter(index++).Value = jobItem.Status.ToString(); + cmd.GetParameter(index++).Value = jobItem.TargetId; + cmd.GetParameter(index++).Value = jobItem.DateCreated; + cmd.GetParameter(index++).Value = jobItem.Progress; + cmd.GetParameter(index++).Value = _json.SerializeToString(jobItem.AdditionalFiles); + cmd.GetParameter(index++).Value = jobItem.MediaSource == null ? null : _json.SerializeToString(jobItem.MediaSource); + cmd.GetParameter(index++).Value = jobItem.IsMarkedForRemoval; + cmd.GetParameter(index++).Value = jobItem.JobItemIndex; + cmd.GetParameter(index++).Value = jobItem.ItemDateModifiedTicks; + + cmd.Transaction = transaction; + + cmd.ExecuteNonQuery(); + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save record:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } + } + } + } + + private SyncJobItem GetJobItem(IDataReader reader) + { + var info = new SyncJobItem + { + Id = reader.GetGuid(0).ToString("N"), + ItemId = reader.GetString(1) + }; + + if (!reader.IsDBNull(2)) + { + info.ItemName = reader.GetString(2); + } + + if (!reader.IsDBNull(3)) + { + info.MediaSourceId = reader.GetString(3); + } + + info.JobId = reader.GetString(4); + + if (!reader.IsDBNull(5)) + { + info.TemporaryPath = reader.GetString(5); + } + if (!reader.IsDBNull(6)) + { + info.OutputPath = reader.GetString(6); + } + + if (!reader.IsDBNull(7)) + { + info.Status = (SyncJobItemStatus)Enum.Parse(typeof(SyncJobItemStatus), reader.GetString(7), true); + } + + info.TargetId = reader.GetString(8); + + info.DateCreated = reader.GetDateTime(9).ToUniversalTime(); + + if (!reader.IsDBNull(10)) + { + info.Progress = reader.GetDouble(10); + } + + if (!reader.IsDBNull(11)) + { + var json = reader.GetString(11); + + if (!string.IsNullOrWhiteSpace(json)) + { + info.AdditionalFiles = _json.DeserializeFromString>(json); + } + } + + if (!reader.IsDBNull(12)) + { + var json = reader.GetString(12); + + if (!string.IsNullOrWhiteSpace(json)) + { + info.MediaSource = _json.DeserializeFromString(json); + } + } + + info.IsMarkedForRemoval = reader.GetBoolean(13); + info.JobItemIndex = reader.GetInt32(14); + + if (!reader.IsDBNull(15)) + { + info.ItemDateModifiedTicks = reader.GetInt64(15); + } + + return info; + } + } +} diff --git a/Emby.Server.Core/project.json b/Emby.Server.Core/project.json index 4f5adf09f4..e4e4741b13 100644 --- a/Emby.Server.Core/project.json +++ b/Emby.Server.Core/project.json @@ -22,6 +22,12 @@ }, "Emby.Common.Implementations": { "target": "project" + }, + "Mono.Nat": { + "target": "project" + }, + "Emby.Server.Implementations": { + "target": "project" } } }, @@ -29,6 +35,7 @@ "imports": "dnxcore50", "dependencies": { "NETStandard.Library": "1.6.0", + "System.AppDomain": "2.0.11", "MediaBrowser.Model": { "target": "project" }, @@ -40,6 +47,12 @@ }, "Emby.Common.Implementations": { "target": "project" + }, + "Mono.Nat": { + "target": "project" + }, + "Emby.Server.Implementations": { + "target": "project" } } } diff --git a/Emby.Server.Implementations/Collections/CollectionsDynamicFolder.cs b/Emby.Server.Implementations/Collections/CollectionsDynamicFolder.cs new file mode 100644 index 0000000000..4ff33e6451 --- /dev/null +++ b/Emby.Server.Implementations/Collections/CollectionsDynamicFolder.cs @@ -0,0 +1,34 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Entities; +using System.IO; +using MediaBrowser.Common.IO; +using MediaBrowser.Model.IO; +using MediaBrowser.Controller.Collections; +using MediaBrowser.Controller.IO; + +namespace Emby.Server.Implementations.Collections +{ + public class CollectionsDynamicFolder : IVirtualFolderCreator + { + private readonly IApplicationPaths _appPaths; + private readonly IFileSystem _fileSystem; + + public CollectionsDynamicFolder(IApplicationPaths appPaths, IFileSystem fileSystem) + { + _appPaths = appPaths; + _fileSystem = fileSystem; + } + + public BasePluginFolder GetFolder() + { + var path = Path.Combine(_appPaths.DataPath, "collections"); + + _fileSystem.CreateDirectory(path); + + return new ManualCollectionsFolder + { + Path = path + }; + } + } +} diff --git a/Emby.Server.Implementations/Devices/CameraUploadsDynamicFolder.cs b/Emby.Server.Implementations/Devices/CameraUploadsDynamicFolder.cs new file mode 100644 index 0000000000..e2d5d0272f --- /dev/null +++ b/Emby.Server.Implementations/Devices/CameraUploadsDynamicFolder.cs @@ -0,0 +1,41 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Entities; +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Server.Implementations.Devices; + +namespace Emby.Server.Implementations.Devices +{ + public class CameraUploadsDynamicFolder : IVirtualFolderCreator + { + private readonly IApplicationPaths _appPaths; + private readonly IFileSystem _fileSystem; + + public CameraUploadsDynamicFolder(IApplicationPaths appPaths, IFileSystem fileSystem) + { + _appPaths = appPaths; + _fileSystem = fileSystem; + } + + public BasePluginFolder GetFolder() + { + var path = Path.Combine(_appPaths.DataPath, "camerauploads"); + + _fileSystem.CreateDirectory(path); + + return new CameraUploadsFolder + { + Path = path + }; + } + } + +} diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index cb15eca527..438edf212f 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -43,11 +43,13 @@ + + @@ -175,6 +177,7 @@ + @@ -291,114 +294,6 @@ - - swagger-ui\lib\backbone-min.js - PreserveNewest - - - swagger-ui\lib\handlebars-2.0.0.js - PreserveNewest - - - swagger-ui\lib\highlight.7.3.pack.js - PreserveNewest - - - swagger-ui\lib\jquery-1.8.0.min.js - PreserveNewest - - - swagger-ui\lib\jquery.ba-bbq.min.js - PreserveNewest - - - swagger-ui\lib\jquery.slideto.min.js - PreserveNewest - - - swagger-ui\lib\jquery.wiggle.min.js - PreserveNewest - - - swagger-ui\lib\marked.js - PreserveNewest - - - swagger-ui\lib\shred.bundle.js - PreserveNewest - - - swagger-ui\lib\swagger-client.js - PreserveNewest - - - swagger-ui\lib\swagger-oauth.js - PreserveNewest - - - swagger-ui\lib\underscore-min.js - PreserveNewest - - - swagger-ui\o2c.html - PreserveNewest - - - swagger-ui\patch.js - PreserveNewest - - - swagger-ui\swagger-ui.js - PreserveNewest - - - swagger-ui\swagger-ui.min.js - PreserveNewest - - - swagger-ui\fonts\droid-sans-v6-latin-700.eot - PreserveNewest - - - swagger-ui\fonts\droid-sans-v6-latin-700.ttf - PreserveNewest - - - swagger-ui\fonts\droid-sans-v6-latin-700.woff - PreserveNewest - - - swagger-ui\fonts\droid-sans-v6-latin-700.woff2 - PreserveNewest - - - swagger-ui\fonts\droid-sans-v6-latin-regular.eot - PreserveNewest - - - swagger-ui\fonts\droid-sans-v6-latin-regular.ttf - PreserveNewest - - - swagger-ui\fonts\droid-sans-v6-latin-regular.woff - PreserveNewest - - - swagger-ui\fonts\droid-sans-v6-latin-regular.woff2 - PreserveNewest - - - swagger-ui\css\reset.css - PreserveNewest - - - swagger-ui\css\screen.css - PreserveNewest - - - swagger-ui\css\typography.css - PreserveNewest - diff --git a/Emby.Server.Implementations/Playlists/PlaylistsDynamicFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsDynamicFolder.cs new file mode 100644 index 0000000000..dacc937e1a --- /dev/null +++ b/Emby.Server.Implementations/Playlists/PlaylistsDynamicFolder.cs @@ -0,0 +1,32 @@ +using System.IO; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Server.Implementations.Playlists; + +namespace Emby.Server.Implementations.Playlists +{ + public class PlaylistsDynamicFolder : IVirtualFolderCreator + { + private readonly IApplicationPaths _appPaths; + private readonly IFileSystem _fileSystem; + + public PlaylistsDynamicFolder(IApplicationPaths appPaths, IFileSystem fileSystem) + { + _appPaths = appPaths; + _fileSystem = fileSystem; + } + + public BasePluginFolder GetFolder() + { + var path = Path.Combine(_appPaths.DataPath, "playlists"); + + _fileSystem.CreateDirectory(path); + + return new PlaylistsFolder + { + Path = path + }; + } + } +} diff --git a/MediaBrowser.Controller/Devices/CameraUploadsFolder.cs b/MediaBrowser.Controller/Devices/CameraUploadsFolder.cs new file mode 100644 index 0000000000..979a929ca7 --- /dev/null +++ b/MediaBrowser.Controller/Devices/CameraUploadsFolder.cs @@ -0,0 +1,67 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Entities; +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Server.Implementations.Devices +{ + public class CameraUploadsFolder : BasePluginFolder, ISupportsUserSpecificView + { + public CameraUploadsFolder() + { + Name = "Camera Uploads"; + } + + public override bool IsVisible(User user) + { + if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) + { + return false; + } + + return base.IsVisible(user) && HasChildren(); + } + + [IgnoreDataMember] + public override string CollectionType + { + get { return Model.Entities.CollectionType.Photos; } + } + + public override string GetClientTypeName() + { + return typeof(CollectionFolder).Name; + } + + private bool? _hasChildren; + private bool HasChildren() + { + if (!_hasChildren.HasValue) + { + _hasChildren = LibraryManager.GetItemIds(new InternalItemsQuery { ParentId = Id }).Count > 0; + } + + return _hasChildren.Value; + } + + protected override Task ValidateChildrenInternal(IProgress progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + { + _hasChildren = null; + return base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService); + } + + [IgnoreDataMember] + public bool EnableUserSpecificView + { + get { return true; } + } + } +} diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 518daa6d75..d867b15123 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -77,6 +77,7 @@ + @@ -217,6 +218,7 @@ + diff --git a/MediaBrowser.Controller/Playlists/ManualPlaylistsFolder.cs b/MediaBrowser.Controller/Playlists/ManualPlaylistsFolder.cs new file mode 100644 index 0000000000..07773d846b --- /dev/null +++ b/MediaBrowser.Controller/Playlists/ManualPlaylistsFolder.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Server.Implementations.Playlists +{ + public class PlaylistsFolder : BasePluginFolder + { + public PlaylistsFolder() + { + Name = "Playlists"; + } + + public override bool IsVisible(User user) + { + return base.IsVisible(user) && GetChildren(user, true).Any(); + } + + protected override IEnumerable GetEligibleChildrenForRecursiveChildren(User user) + { + return base.GetEligibleChildrenForRecursiveChildren(user).OfType(); + } + + [IgnoreDataMember] + public override bool IsHidden + { + get + { + return true; + } + } + + [IgnoreDataMember] + public override string CollectionType + { + get { return MediaBrowser.Model.Entities.CollectionType.Playlists; } + } + + protected override Task> GetItemsInternal(InternalItemsQuery query) + { + query.Recursive = false; + return base.GetItemsInternal(query); + } + } +} + diff --git a/MediaBrowser.Server.Implementations/Collections/CollectionsDynamicFolder.cs b/MediaBrowser.Server.Implementations/Collections/CollectionsDynamicFolder.cs deleted file mode 100644 index 2d0c3642ea..0000000000 --- a/MediaBrowser.Server.Implementations/Collections/CollectionsDynamicFolder.cs +++ /dev/null @@ -1,34 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Entities; -using System.IO; -using MediaBrowser.Common.IO; -using MediaBrowser.Model.IO; -using MediaBrowser.Controller.Collections; -using MediaBrowser.Controller.IO; - -namespace MediaBrowser.Server.Implementations.Collections -{ - public class CollectionsDynamicFolder : IVirtualFolderCreator - { - private readonly IApplicationPaths _appPaths; - private readonly IFileSystem _fileSystem; - - public CollectionsDynamicFolder(IApplicationPaths appPaths, IFileSystem fileSystem) - { - _appPaths = appPaths; - _fileSystem = fileSystem; - } - - public BasePluginFolder GetFolder() - { - var path = Path.Combine(_appPaths.DataPath, "collections"); - - _fileSystem.CreateDirectory(path); - - return new ManualCollectionsFolder - { - Path = path - }; - } - } -} diff --git a/MediaBrowser.Server.Implementations/Devices/CameraUploadsFolder.cs b/MediaBrowser.Server.Implementations/Devices/CameraUploadsFolder.cs deleted file mode 100644 index 4a85ecf086..0000000000 --- a/MediaBrowser.Server.Implementations/Devices/CameraUploadsFolder.cs +++ /dev/null @@ -1,92 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Entities; -using System; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.IO; -using MediaBrowser.Controller.IO; -using MediaBrowser.Model.IO; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Serialization; - -namespace MediaBrowser.Server.Implementations.Devices -{ - public class CameraUploadsFolder : BasePluginFolder, ISupportsUserSpecificView - { - public CameraUploadsFolder() - { - Name = "Camera Uploads"; - } - - public override bool IsVisible(User user) - { - if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) - { - return false; - } - - return base.IsVisible(user) && HasChildren(); - } - - [IgnoreDataMember] - public override string CollectionType - { - get { return Model.Entities.CollectionType.Photos; } - } - - public override string GetClientTypeName() - { - return typeof(CollectionFolder).Name; - } - - private bool? _hasChildren; - private bool HasChildren() - { - if (!_hasChildren.HasValue) - { - _hasChildren = LibraryManager.GetItemIds(new InternalItemsQuery { ParentId = Id }).Count > 0; - } - - return _hasChildren.Value; - } - - protected override Task ValidateChildrenInternal(IProgress progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) - { - _hasChildren = null; - return base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService); - } - - [IgnoreDataMember] - public bool EnableUserSpecificView - { - get { return true; } - } - } - - public class CameraUploadsDynamicFolder : IVirtualFolderCreator - { - private readonly IApplicationPaths _appPaths; - private readonly IFileSystem _fileSystem; - - public CameraUploadsDynamicFolder(IApplicationPaths appPaths, IFileSystem fileSystem) - { - _appPaths = appPaths; - _fileSystem = fileSystem; - } - - public BasePluginFolder GetFolder() - { - var path = Path.Combine(_appPaths.DataPath, "camerauploads"); - - _fileSystem.CreateDirectory(path); - - return new CameraUploadsFolder - { - Path = path - }; - } - } - -} diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index c75ab4c1cb..16c45f2e51 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -44,9 +44,6 @@ Properties\SharedVersion.cs - - - diff --git a/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs b/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs deleted file mode 100644 index 6a910a7bfe..0000000000 --- a/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Playlists; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; - -namespace MediaBrowser.Server.Implementations.Playlists -{ - public class PlaylistsFolder : BasePluginFolder - { - public PlaylistsFolder() - { - Name = "Playlists"; - } - - public override bool IsVisible(User user) - { - return base.IsVisible(user) && GetChildren(user, true).Any(); - } - - protected override IEnumerable GetEligibleChildrenForRecursiveChildren(User user) - { - return base.GetEligibleChildrenForRecursiveChildren(user).OfType(); - } - - [IgnoreDataMember] - public override bool IsHidden - { - get - { - return true; - } - } - - [IgnoreDataMember] - public override string CollectionType - { - get { return MediaBrowser.Model.Entities.CollectionType.Playlists; } - } - - protected override Task> GetItemsInternal(InternalItemsQuery query) - { - query.Recursive = false; - return base.GetItemsInternal(query); - } - } - - public class PlaylistsDynamicFolder : IVirtualFolderCreator - { - private readonly IApplicationPaths _appPaths; - private readonly IFileSystem _fileSystem; - - public PlaylistsDynamicFolder(IApplicationPaths appPaths, IFileSystem fileSystem) - { - _appPaths = appPaths; - _fileSystem = fileSystem; - } - - public BasePluginFolder GetFolder() - { - var path = Path.Combine(_appPaths.DataPath, "playlists"); - - _fileSystem.CreateDirectory(path); - - return new PlaylistsFolder - { - Path = path - }; - } - } -} - diff --git a/MediaBrowser.Server.Mono/Native/DbConnector.cs b/MediaBrowser.Server.Mono/Native/DbConnector.cs index b53aecc786..f25955c91e 100644 --- a/MediaBrowser.Server.Mono/Native/DbConnector.cs +++ b/MediaBrowser.Server.Mono/Native/DbConnector.cs @@ -1,7 +1,7 @@ using System.Data; using System.Threading.Tasks; +using Emby.Server.Core.Data; using MediaBrowser.Model.Logging; -using MediaBrowser.Server.Startup.Common.Persistence; namespace MediaBrowser.Server.Mono.Native { diff --git a/MediaBrowser.Server.Mono/Native/MonoApp.cs b/MediaBrowser.Server.Mono/Native/MonoApp.cs index 64d8488ab9..59c95316c1 100644 --- a/MediaBrowser.Server.Mono/Native/MonoApp.cs +++ b/MediaBrowser.Server.Mono/Native/MonoApp.cs @@ -8,8 +8,9 @@ using System.Collections.Generic; using System.Reflection; using System.Text.RegularExpressions; using Emby.Server.Core; +using Emby.Server.Core.Data; +using Emby.Server.Core.FFMpeg; using MediaBrowser.Model.System; -using MediaBrowser.Server.Startup.Common.Persistence; using MediaBrowser.Server.Startup.Common.FFMpeg; using MediaBrowser.Server.Startup.Common.Networking; using OperatingSystem = MediaBrowser.Server.Startup.Common.OperatingSystem; diff --git a/MediaBrowser.Server.Startup.Common/Activity/ActivityRepository.cs b/MediaBrowser.Server.Startup.Common/Activity/ActivityRepository.cs deleted file mode 100644 index 128aff5912..0000000000 --- a/MediaBrowser.Server.Startup.Common/Activity/ActivityRepository.cs +++ /dev/null @@ -1,252 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Globalization; -using System.IO; -using System.Threading.Tasks; -using MediaBrowser.Controller; -using MediaBrowser.Model.Activity; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Querying; -using MediaBrowser.Server.Startup.Common.Persistence; - -namespace MediaBrowser.Server.Startup.Common.Activity -{ - public class ActivityRepository : BaseSqliteRepository, IActivityRepository - { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - public ActivityRepository(ILogManager logManager, IServerApplicationPaths appPaths, IDbConnector connector) - : base(logManager, connector) - { - DbFilePath = Path.Combine(appPaths.DataPath, "activitylog.db"); - } - - public async Task Initialize() - { - using (var connection = await CreateConnection().ConfigureAwait(false)) - { - string[] queries = { - - "create table if not exists ActivityLogEntries (Id GUID PRIMARY KEY, Name TEXT, Overview TEXT, ShortOverview TEXT, Type TEXT, ItemId TEXT, UserId TEXT, DateCreated DATETIME, LogSeverity TEXT)", - "create index if not exists idx_ActivityLogEntries on ActivityLogEntries(Id)" - }; - - connection.RunQueries(queries, Logger); - } - } - - private const string BaseActivitySelectText = "select Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity from ActivityLogEntries"; - - public Task Create(ActivityLogEntry entry) - { - return Update(entry); - } - - public async Task Update(ActivityLogEntry entry) - { - if (entry == null) - { - throw new ArgumentNullException("entry"); - } - - using (var connection = await CreateConnection().ConfigureAwait(false)) - { - using (var saveActivityCommand = connection.CreateCommand()) - { - saveActivityCommand.CommandText = "replace into ActivityLogEntries (Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) values (@Id, @Name, @Overview, @ShortOverview, @Type, @ItemId, @UserId, @DateCreated, @LogSeverity)"; - - saveActivityCommand.Parameters.Add(saveActivityCommand, "@Id"); - saveActivityCommand.Parameters.Add(saveActivityCommand, "@Name"); - saveActivityCommand.Parameters.Add(saveActivityCommand, "@Overview"); - saveActivityCommand.Parameters.Add(saveActivityCommand, "@ShortOverview"); - saveActivityCommand.Parameters.Add(saveActivityCommand, "@Type"); - saveActivityCommand.Parameters.Add(saveActivityCommand, "@ItemId"); - saveActivityCommand.Parameters.Add(saveActivityCommand, "@UserId"); - saveActivityCommand.Parameters.Add(saveActivityCommand, "@DateCreated"); - saveActivityCommand.Parameters.Add(saveActivityCommand, "@LogSeverity"); - - IDbTransaction transaction = null; - - try - { - transaction = connection.BeginTransaction(); - - var index = 0; - - saveActivityCommand.GetParameter(index++).Value = new Guid(entry.Id); - saveActivityCommand.GetParameter(index++).Value = entry.Name; - saveActivityCommand.GetParameter(index++).Value = entry.Overview; - saveActivityCommand.GetParameter(index++).Value = entry.ShortOverview; - saveActivityCommand.GetParameter(index++).Value = entry.Type; - saveActivityCommand.GetParameter(index++).Value = entry.ItemId; - saveActivityCommand.GetParameter(index++).Value = entry.UserId; - saveActivityCommand.GetParameter(index++).Value = entry.Date; - saveActivityCommand.GetParameter(index++).Value = entry.Severity.ToString(); - - saveActivityCommand.Transaction = transaction; - - saveActivityCommand.ExecuteNonQuery(); - - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save record:", e); - - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } - } - } - } - } - - public QueryResult GetActivityLogEntries(DateTime? minDate, int? startIndex, int? limit) - { - using (var connection = CreateConnection(true).Result) - { - using (var cmd = connection.CreateCommand()) - { - cmd.CommandText = BaseActivitySelectText; - - var whereClauses = new List(); - - if (minDate.HasValue) - { - whereClauses.Add("DateCreated>=@DateCreated"); - cmd.Parameters.Add(cmd, "@DateCreated", DbType.Date).Value = minDate.Value; - } - - var whereTextWithoutPaging = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - if (startIndex.HasValue && startIndex.Value > 0) - { - var pagingWhereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM ActivityLogEntries {0} ORDER BY DateCreated DESC LIMIT {1})", - pagingWhereText, - startIndex.Value.ToString(_usCulture))); - } - - var whereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - cmd.CommandText += whereText; - - cmd.CommandText += " ORDER BY DateCreated DESC"; - - if (limit.HasValue) - { - cmd.CommandText += " LIMIT " + limit.Value.ToString(_usCulture); - } - - cmd.CommandText += "; select count (Id) from ActivityLogEntries" + whereTextWithoutPaging; - - var list = new List(); - var count = 0; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) - { - while (reader.Read()) - { - list.Add(GetEntry(reader)); - } - - if (reader.NextResult() && reader.Read()) - { - count = reader.GetInt32(0); - } - } - - return new QueryResult() - { - Items = list.ToArray(), - TotalRecordCount = count - }; - } - } - } - - private ActivityLogEntry GetEntry(IDataReader reader) - { - var index = 0; - - var info = new ActivityLogEntry - { - Id = reader.GetGuid(index).ToString("N") - }; - - index++; - if (!reader.IsDBNull(index)) - { - info.Name = reader.GetString(index); - } - - index++; - if (!reader.IsDBNull(index)) - { - info.Overview = reader.GetString(index); - } - - index++; - if (!reader.IsDBNull(index)) - { - info.ShortOverview = reader.GetString(index); - } - - index++; - if (!reader.IsDBNull(index)) - { - info.Type = reader.GetString(index); - } - - index++; - if (!reader.IsDBNull(index)) - { - info.ItemId = reader.GetString(index); - } - - index++; - if (!reader.IsDBNull(index)) - { - info.UserId = reader.GetString(index); - } - - index++; - info.Date = reader.GetDateTime(index).ToUniversalTime(); - - index++; - if (!reader.IsDBNull(index)) - { - info.Severity = (LogSeverity)Enum.Parse(typeof(LogSeverity), reader.GetString(index), true); - } - - return info; - } - } -} diff --git a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs index f0c134c081..6d7ec699d9 100644 --- a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs +++ b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs @@ -51,7 +51,6 @@ using MediaBrowser.Providers.Subtitles; using MediaBrowser.Server.Implementations; using MediaBrowser.Server.Implementations.Devices; using MediaBrowser.Server.Startup.Common.FFMpeg; -using MediaBrowser.Server.Startup.Common.Migrations; using MediaBrowser.WebDashboard.Api; using MediaBrowser.XbmcMetadata.Providers; using System; @@ -90,6 +89,14 @@ using Emby.Dlna.Main; using Emby.Dlna.MediaReceiverRegistrar; using Emby.Dlna.Ssdp; using Emby.Server.Core; +using Emby.Server.Core.Activity; +using Emby.Server.Core.Configuration; +using Emby.Server.Core.Data; +using Emby.Server.Core.Devices; +using Emby.Server.Core.Migrations; +using Emby.Server.Core.Notifications; +using Emby.Server.Core.Social; +using Emby.Server.Core.Sync; using Emby.Server.Implementations.Activity; using Emby.Server.Implementations.Channels; using Emby.Server.Implementations.Collections; @@ -126,15 +133,8 @@ using MediaBrowser.Model.Services; using MediaBrowser.Model.Social; using MediaBrowser.Model.Text; using MediaBrowser.Model.Xml; -using MediaBrowser.Server.Startup.Common.Activity; -using MediaBrowser.Server.Startup.Common.Configuration; -using MediaBrowser.Server.Startup.Common.Devices; using MediaBrowser.Server.Startup.Common.IO; -using MediaBrowser.Server.Startup.Common.Notifications; -using MediaBrowser.Server.Startup.Common.Persistence; using MediaBrowser.Server.Startup.Common.Security; -using MediaBrowser.Server.Startup.Common.Social; -using MediaBrowser.Server.Startup.Common.Sync; using OpenSubtitlesHandler; using ServiceStack; using SocketHttpListener.Primitives; diff --git a/MediaBrowser.Server.Startup.Common/Devices/DeviceRepository.cs b/MediaBrowser.Server.Startup.Common/Devices/DeviceRepository.cs deleted file mode 100644 index 633faeef9d..0000000000 --- a/MediaBrowser.Server.Startup.Common/Devices/DeviceRepository.cs +++ /dev/null @@ -1,208 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Model.Devices; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Session; - -namespace MediaBrowser.Server.Startup.Common.Devices -{ - public class DeviceRepository : IDeviceRepository - { - private readonly object _syncLock = new object(); - - private readonly IApplicationPaths _appPaths; - private readonly IJsonSerializer _json; - private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; - - private Dictionary _devices; - - public DeviceRepository(IApplicationPaths appPaths, IJsonSerializer json, ILogger logger, IFileSystem fileSystem) - { - _appPaths = appPaths; - _json = json; - _logger = logger; - _fileSystem = fileSystem; - } - - private string GetDevicesPath() - { - return Path.Combine(_appPaths.DataPath, "devices"); - } - - private string GetDevicePath(string id) - { - return Path.Combine(GetDevicesPath(), id.GetMD5().ToString("N")); - } - - public Task SaveDevice(DeviceInfo device) - { - var path = Path.Combine(GetDevicePath(device.Id), "device.json"); - _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); - - lock (_syncLock) - { - _json.SerializeToFile(device, path); - _devices[device.Id] = device; - } - return Task.FromResult(true); - } - - public Task SaveCapabilities(string reportedId, ClientCapabilities capabilities) - { - var device = GetDevice(reportedId); - - if (device == null) - { - throw new ArgumentException("No device has been registed with id " + reportedId); - } - - device.Capabilities = capabilities; - SaveDevice(device); - - return Task.FromResult(true); - } - - public ClientCapabilities GetCapabilities(string reportedId) - { - var device = GetDevice(reportedId); - - return device == null ? null : device.Capabilities; - } - - public DeviceInfo GetDevice(string id) - { - if (string.IsNullOrWhiteSpace(id)) - { - throw new ArgumentNullException("id"); - } - - return GetDevices() - .FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)); - } - - public IEnumerable GetDevices() - { - lock (_syncLock) - { - if (_devices == null) - { - _devices = new Dictionary(StringComparer.OrdinalIgnoreCase); - - var devices = LoadDevices().ToList(); - foreach (var device in devices) - { - _devices[device.Id] = device; - } - } - return _devices.Values.ToList(); - } - } - - private IEnumerable LoadDevices() - { - var path = GetDevicesPath(); - - try - { - return _fileSystem - .GetFilePaths(path, true) - .Where(i => string.Equals(Path.GetFileName(i), "device.json", StringComparison.OrdinalIgnoreCase)) - .ToList() - .Select(i => - { - try - { - return _json.DeserializeFromFile(i); - } - catch (Exception ex) - { - _logger.ErrorException("Error reading {0}", ex, i); - return null; - } - }) - .Where(i => i != null); - } - catch (IOException) - { - return new List(); - } - } - - public Task DeleteDevice(string id) - { - var path = GetDevicePath(id); - - lock (_syncLock) - { - try - { - _fileSystem.DeleteDirectory(path, true); - } - catch (DirectoryNotFoundException) - { - } - - _devices = null; - } - - return Task.FromResult(true); - } - - public ContentUploadHistory GetCameraUploadHistory(string deviceId) - { - var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json"); - - lock (_syncLock) - { - try - { - return _json.DeserializeFromFile(path); - } - catch (IOException) - { - return new ContentUploadHistory - { - DeviceId = deviceId - }; - } - } - } - - public void AddCameraUpload(string deviceId, LocalFileInfo file) - { - var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json"); - _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); - - lock (_syncLock) - { - ContentUploadHistory history; - - try - { - history = _json.DeserializeFromFile(path); - } - catch (IOException) - { - history = new ContentUploadHistory - { - DeviceId = deviceId - }; - } - - history.DeviceId = deviceId; - history.FilesUploaded.Add(file); - - _json.SerializeToFile(history, path); - } - } - } -} diff --git a/MediaBrowser.Server.Startup.Common/EntryPoints/ExternalPortForwarding.cs b/MediaBrowser.Server.Startup.Common/EntryPoints/ExternalPortForwarding.cs deleted file mode 100644 index 03e26ee863..0000000000 --- a/MediaBrowser.Server.Startup.Common/EntryPoints/ExternalPortForwarding.cs +++ /dev/null @@ -1,292 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Net; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Threading; -using Mono.Nat; - -namespace MediaBrowser.Server.Startup.Common.EntryPoints -{ - public class ExternalPortForwarding : IServerEntryPoint - { - private readonly IServerApplicationHost _appHost; - private readonly ILogger _logger; - private readonly IHttpClient _httpClient; - private readonly IServerConfigurationManager _config; - private readonly IDeviceDiscovery _deviceDiscovery; - - private ITimer _timer; - private bool _isStarted; - private readonly ITimerFactory _timerFactory; - - public ExternalPortForwarding(ILogManager logmanager, IServerApplicationHost appHost, IServerConfigurationManager config, IDeviceDiscovery deviceDiscovery, IHttpClient httpClient, ITimerFactory timerFactory) - { - _logger = logmanager.GetLogger("PortMapper"); - _appHost = appHost; - _config = config; - _deviceDiscovery = deviceDiscovery; - _httpClient = httpClient; - _timerFactory = timerFactory; - } - - private string _lastConfigIdentifier; - private string GetConfigIdentifier() - { - var values = new List(); - var config = _config.Configuration; - - values.Add(config.EnableUPnP.ToString()); - values.Add(config.PublicPort.ToString(CultureInfo.InvariantCulture)); - values.Add(_appHost.HttpPort.ToString(CultureInfo.InvariantCulture)); - values.Add(_appHost.HttpsPort.ToString(CultureInfo.InvariantCulture)); - values.Add(config.EnableHttps.ToString()); - values.Add(_appHost.EnableHttps.ToString()); - - return string.Join("|", values.ToArray()); - } - - void _config_ConfigurationUpdated(object sender, EventArgs e) - { - if (!string.Equals(_lastConfigIdentifier, GetConfigIdentifier(), StringComparison.OrdinalIgnoreCase)) - { - if (_isStarted) - { - DisposeNat(); - } - - Run(); - } - } - - public void Run() - { - NatUtility.Logger = _logger; - NatUtility.HttpClient = _httpClient; - - if (_config.Configuration.EnableUPnP) - { - Start(); - } - - _config.ConfigurationUpdated -= _config_ConfigurationUpdated; - _config.ConfigurationUpdated += _config_ConfigurationUpdated; - } - - private void Start() - { - _logger.Debug("Starting NAT discovery"); - NatUtility.EnabledProtocols = new List - { - NatProtocol.Pmp - }; - NatUtility.DeviceFound += NatUtility_DeviceFound; - - // Mono.Nat does never rise this event. The event is there however it is useless. - // You could remove it with no risk. - NatUtility.DeviceLost += NatUtility_DeviceLost; - - - NatUtility.StartDiscovery(); - - _timer = _timerFactory.Create(ClearCreatedRules, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); - - _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; - - _lastConfigIdentifier = GetConfigIdentifier(); - - _isStarted = true; - } - - private async void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs e) - { - var info = e.Argument; - - string usn; - if (!info.Headers.TryGetValue("USN", out usn)) usn = string.Empty; - - string nt; - if (!info.Headers.TryGetValue("NT", out nt)) nt = string.Empty; - - // Filter device type - if (usn.IndexOf("WANIPConnection:", StringComparison.OrdinalIgnoreCase) == -1 && - nt.IndexOf("WANIPConnection:", StringComparison.OrdinalIgnoreCase) == -1 && - usn.IndexOf("WANPPPConnection:", StringComparison.OrdinalIgnoreCase) == -1 && - nt.IndexOf("WANPPPConnection:", StringComparison.OrdinalIgnoreCase) == -1) - { - return; - } - - var identifier = string.IsNullOrWhiteSpace(usn) ? nt : usn; - - if (info.Location == null) - { - return; - } - - lock (_usnsHandled) - { - if (_usnsHandled.Contains(identifier)) - { - return; - } - _usnsHandled.Add(identifier); - } - - _logger.Debug("Found NAT device: " + identifier); - - IPAddress address; - if (IPAddress.TryParse(info.Location.Host, out address)) - { - // The Handle method doesn't need the port - var endpoint = new IPEndPoint(address, info.Location.Port); - - IPAddress localAddress = null; - - try - { - var localAddressString = await _appHost.GetLocalApiUrl().ConfigureAwait(false); - - Uri uri; - if (Uri.TryCreate(localAddressString, UriKind.Absolute, out uri)) - { - localAddressString = uri.Host; - - if (!IPAddress.TryParse(localAddressString, out localAddress)) - { - return; - } - } - } - catch (Exception ex) - { - return; - } - - _logger.Debug("Calling Nat.Handle on " + identifier); - NatUtility.Handle(localAddress, info, endpoint, NatProtocol.Upnp); - } - } - - private void ClearCreatedRules(object state) - { - _createdRules = new List(); - lock (_usnsHandled) - { - _usnsHandled.Clear(); - } - } - - void NatUtility_DeviceFound(object sender, DeviceEventArgs e) - { - try - { - var device = e.Device; - _logger.Debug("NAT device found: {0}", device.LocalAddress.ToString()); - - CreateRules(device); - } - catch - { - // I think it could be a good idea to log the exception because - // you are using permanent portmapping here (never expire) and that means that next time - // CreatePortMap is invoked it can fails with a 718-ConflictInMappingEntry or not. That depends - // on the router's upnp implementation (specs says it should fail however some routers don't do it) - // It also can fail with others like 727-ExternalPortOnlySupportsWildcard, 728-NoPortMapsAvailable - // and those errors (upnp errors) could be useful for diagnosting. - - // Commenting out because users are reporting problems out of our control - //_logger.ErrorException("Error creating port forwarding rules", ex); - } - } - - private List _createdRules = new List(); - private List _usnsHandled = new List(); - private void CreateRules(INatDevice device) - { - // On some systems the device discovered event seems to fire repeatedly - // This check will help ensure we're not trying to port map the same device over and over - - var address = device.LocalAddress.ToString(); - - if (!_createdRules.Contains(address)) - { - _createdRules.Add(address); - - CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort); - CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort); - } - } - - private async void CreatePortMap(INatDevice device, int privatePort, int publicPort) - { - _logger.Debug("Creating port map on port {0}", privatePort); - - try - { - await device.CreatePortMap(new Mapping(Protocol.Tcp, privatePort, publicPort) - { - Description = _appHost.Name - }).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error creating port map", ex); - } - } - - // As I said before, this method will be never invoked. You can remove it. - void NatUtility_DeviceLost(object sender, DeviceEventArgs e) - { - var device = e.Device; - _logger.Debug("NAT device lost: {0}", device.LocalAddress.ToString()); - } - - public void Dispose() - { - DisposeNat(); - } - - private void DisposeNat() - { - _logger.Debug("Stopping NAT discovery"); - - if (_timer != null) - { - _timer.Dispose(); - _timer = null; - } - - _deviceDiscovery.DeviceDiscovered -= _deviceDiscovery_DeviceDiscovered; - - try - { - // This is not a significant improvement - NatUtility.StopDiscovery(); - NatUtility.DeviceFound -= NatUtility_DeviceFound; - NatUtility.DeviceLost -= NatUtility_DeviceLost; - } - // Statements in try-block will no fail because StopDiscovery is a one-line - // method that was no chances to fail. - // public static void StopDiscovery () - // { - // searching.Reset(); - // } - // IMO you could remove the catch-block - catch (Exception ex) - { - _logger.ErrorException("Error stopping NAT Discovery", ex); - } - finally - { - _isStarted = false; - } - } - } -} \ No newline at end of file diff --git a/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegInfo.cs b/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegInfo.cs deleted file mode 100644 index d33b12a9a7..0000000000 --- a/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegInfo.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace MediaBrowser.Server.Startup.Common.FFMpeg -{ - /// - /// Class FFMpegInfo - /// - public class FFMpegInfo - { - /// - /// Gets or sets the path. - /// - /// The path. - public string EncoderPath { get; set; } - /// - /// Gets or sets the probe path. - /// - /// The probe path. - public string ProbePath { get; set; } - /// - /// Gets or sets the version. - /// - /// The version. - public string Version { get; set; } - } -} \ No newline at end of file diff --git a/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegInstallInfo.cs b/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegInstallInfo.cs deleted file mode 100644 index a2a44f805c..0000000000 --- a/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegInstallInfo.cs +++ /dev/null @@ -1,20 +0,0 @@ - -namespace MediaBrowser.Server.Startup.Common.FFMpeg -{ - public class FFMpegInstallInfo - { - public string Version { get; set; } - public string FFMpegFilename { get; set; } - public string FFProbeFilename { get; set; } - public string ArchiveType { get; set; } - public string[] DownloadUrls { get; set; } - - public FFMpegInstallInfo() - { - DownloadUrls = new string[] { }; - Version = "Path"; - FFMpegFilename = "ffmpeg"; - FFProbeFilename = "ffprobe"; - } - } -} \ No newline at end of file diff --git a/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegLoader.cs b/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegLoader.cs index e40dfe45f8..08e6b435b5 100644 --- a/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegLoader.cs +++ b/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegLoader.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Emby.Server.Core; +using Emby.Server.Core.FFMpeg; namespace MediaBrowser.Server.Startup.Common.FFMpeg { diff --git a/MediaBrowser.Server.Startup.Common/INativeApp.cs b/MediaBrowser.Server.Startup.Common/INativeApp.cs index 4dd6ecfe1b..182afbf003 100644 --- a/MediaBrowser.Server.Startup.Common/INativeApp.cs +++ b/MediaBrowser.Server.Startup.Common/INativeApp.cs @@ -3,8 +3,8 @@ using MediaBrowser.Model.Logging; using System.Collections.Generic; using System.Reflection; using Emby.Server.Core; -using MediaBrowser.Server.Startup.Common.FFMpeg; -using MediaBrowser.Server.Startup.Common.Persistence; +using Emby.Server.Core.Data; +using Emby.Server.Core.FFMpeg; namespace MediaBrowser.Server.Startup.Common { diff --git a/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj b/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj index 6196a74a2b..79c1356de3 100644 --- a/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj +++ b/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj @@ -93,14 +93,27 @@ Properties\SharedVersion.cs - - - + + + + + + + + + + + + + + + + + + - - @@ -128,44 +141,10 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -408,6 +387,9 @@ + + +