From 7c5b22246357ef6bcd6d74e61a8fe6a8a9d4df3e Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Tue, 28 Jan 2014 16:25:10 -0500 Subject: Created ILibraryMonitor to replace IDirectoryWatchers --- .../IO/LibraryMonitor.cs | 569 +++++++++++++++++++++ 1 file changed, 569 insertions(+) create mode 100644 MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs (limited to 'MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs') diff --git a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs new file mode 100644 index 000000000..e09e66765 --- /dev/null +++ b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs @@ -0,0 +1,569 @@ +using MediaBrowser.Common.ScheduledTasks; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Server.Implementations.ScheduledTasks; +using Microsoft.Win32; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.IO +{ + public class LibraryMonitor : ILibraryMonitor + { + /// + /// The file system watchers + /// + private readonly ConcurrentDictionary _fileSystemWatchers = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + /// + /// The update timer + /// + private Timer _updateTimer; + /// + /// The affected paths + /// + private readonly ConcurrentDictionary _affectedPaths = new ConcurrentDictionary(); + + /// + /// A dynamic list of paths that should be ignored. Added to during our own file sytem modifications. + /// + private readonly ConcurrentDictionary _tempIgnoredPaths = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Any file name ending in any of these will be ignored by the watchers + /// + private readonly IReadOnlyList _alwaysIgnoreFiles = new List { "thumbs.db", "small.jpg", "albumart.jpg" }; + + /// + /// The timer lock + /// + private readonly object _timerLock = new object(); + + /// + /// Add the path to our temporary ignore list. Use when writing to a path within our listening scope. + /// + /// The path. + private void TemporarilyIgnore(string path) + { + _tempIgnoredPaths[path] = path; + } + + /// + /// Removes the temp ignore. + /// + /// The path. + private async void RemoveTempIgnore(string path) + { + // This is an arbitraty amount of time, but delay it because file system writes often trigger events after RemoveTempIgnore has been called. + // Seeing long delays in some situations, especially over the network. + // Seeing delays up to 40 seconds, but not going to ignore changes for that long. + await Task.Delay(1500).ConfigureAwait(false); + + string val; + _tempIgnoredPaths.TryRemove(path, out val); + } + + public void ReportFileSystemChangeBeginning(string path) + { + TemporarilyIgnore(path); + } + + public void ReportFileSystemChangeComplete(string path, bool refreshPath) + { + RemoveTempIgnore(path); + + if (refreshPath) + { + ReportFileSystemChanged(path); + } + } + + /// + /// Gets or sets the logger. + /// + /// The logger. + private ILogger Logger { get; set; } + + /// + /// Gets or sets the task manager. + /// + /// The task manager. + private ITaskManager TaskManager { get; set; } + + private ILibraryManager LibraryManager { get; set; } + private IServerConfigurationManager ConfigurationManager { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public LibraryMonitor(ILogManager logManager, ITaskManager taskManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager) + { + if (taskManager == null) + { + throw new ArgumentNullException("taskManager"); + } + + LibraryManager = libraryManager; + TaskManager = taskManager; + Logger = logManager.GetLogger(GetType().Name); + ConfigurationManager = configurationManager; + + SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged; + } + + /// + /// Handles the PowerModeChanged event of the SystemEvents control. + /// + /// The source of the event. + /// The instance containing the event data. + void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e) + { + Stop(); + Start(); + } + + /// + /// Starts this instance. + /// + public void Start() + { + LibraryManager.ItemAdded += LibraryManager_ItemAdded; + LibraryManager.ItemRemoved += LibraryManager_ItemRemoved; + + var pathsToWatch = new List { LibraryManager.RootFolder.Path }; + + var paths = LibraryManager + .RootFolder + .Children + .OfType() + .Where(i => i.LocationType != LocationType.Remote && i.LocationType != LocationType.Virtual) + .SelectMany(f => + { + try + { + // Accessing ResolveArgs could involve file system access + return f.ResolveArgs.PhysicalLocations; + } + catch (IOException) + { + return new string[] { }; + } + + }) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(i => i) + .ToList(); + + foreach (var path in paths) + { + if (!ContainsParentFolder(pathsToWatch, path)) + { + pathsToWatch.Add(path); + } + } + + foreach (var path in pathsToWatch) + { + StartWatchingPath(path); + } + } + + /// + /// Handles the ItemRemoved event of the LibraryManager control. + /// + /// The source of the event. + /// The instance containing the event data. + void LibraryManager_ItemRemoved(object sender, ItemChangeEventArgs e) + { + if (e.Item.Parent is AggregateFolder) + { + StopWatchingPath(e.Item.Path); + } + } + + /// + /// Handles the ItemAdded event of the LibraryManager control. + /// + /// The source of the event. + /// The instance containing the event data. + void LibraryManager_ItemAdded(object sender, ItemChangeEventArgs e) + { + if (e.Item.Parent is AggregateFolder) + { + StartWatchingPath(e.Item.Path); + } + } + + /// + /// Examine a list of strings assumed to be file paths to see if it contains a parent of + /// the provided path. + /// + /// The LST. + /// The path. + /// true if [contains parent folder] [the specified LST]; otherwise, false. + /// path + private static bool ContainsParentFolder(IEnumerable lst, string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + path = path.TrimEnd(Path.DirectorySeparatorChar); + + return lst.Any(str => + { + //this should be a little quicker than examining each actual parent folder... + var compare = str.TrimEnd(Path.DirectorySeparatorChar); + + return (path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar)); + }); + } + + /// + /// Starts the watching path. + /// + /// The path. + private void StartWatchingPath(string path) + { + // Creating a FileSystemWatcher over the LAN can take hundreds of milliseconds, so wrap it in a Task to do them all in parallel + Task.Run(() => + { + var newWatcher = new FileSystemWatcher(path, "*") { IncludeSubdirectories = true, InternalBufferSize = 32767 }; + + newWatcher.Created += watcher_Changed; + newWatcher.Deleted += watcher_Changed; + newWatcher.Renamed += watcher_Changed; + newWatcher.Changed += watcher_Changed; + + newWatcher.Error += watcher_Error; + + try + { + if (_fileSystemWatchers.TryAdd(path, newWatcher)) + { + newWatcher.EnableRaisingEvents = true; + Logger.Info("Watching directory " + path); + } + else + { + Logger.Info("Unable to add directory watcher for {0}. It already exists in the dictionary." + path); + newWatcher.Dispose(); + } + + } + catch (IOException ex) + { + Logger.ErrorException("Error watching path: {0}", ex, path); + } + catch (PlatformNotSupportedException ex) + { + Logger.ErrorException("Error watching path: {0}", ex, path); + } + }); + } + + /// + /// Stops the watching path. + /// + /// The path. + private void StopWatchingPath(string path) + { + FileSystemWatcher watcher; + + if (_fileSystemWatchers.TryGetValue(path, out watcher)) + { + DisposeWatcher(watcher); + } + } + + /// + /// Disposes the watcher. + /// + /// The watcher. + private void DisposeWatcher(FileSystemWatcher watcher) + { + Logger.Info("Stopping directory watching for path {0}", watcher.Path); + + watcher.EnableRaisingEvents = false; + watcher.Dispose(); + + RemoveWatcherFromList(watcher); + } + + /// + /// Removes the watcher from list. + /// + /// The watcher. + private void RemoveWatcherFromList(FileSystemWatcher watcher) + { + FileSystemWatcher removed; + + _fileSystemWatchers.TryRemove(watcher.Path, out removed); + } + + /// + /// Handles the Error event of the watcher control. + /// + /// The source of the event. + /// The instance containing the event data. + void watcher_Error(object sender, ErrorEventArgs e) + { + var ex = e.GetException(); + var dw = (FileSystemWatcher)sender; + + Logger.ErrorException("Error in Directory watcher for: " + dw.Path, ex); + + DisposeWatcher(dw); + } + + /// + /// Handles the Changed event of the watcher control. + /// + /// The source of the event. + /// The instance containing the event data. + void watcher_Changed(object sender, FileSystemEventArgs e) + { + try + { + OnWatcherChanged(e); + } + catch (Exception ex) + { + Logger.ErrorException("Exception in watcher changed. Path: {0}", ex, e.FullPath); + } + } + + private void OnWatcherChanged(FileSystemEventArgs e) + { + Logger.Debug("Watcher sees change of type " + e.ChangeType + " to " + e.FullPath); + + ReportFileSystemChanged(e.FullPath); + } + + public void ReportFileSystemChanged(string path) + { + var filename = Path.GetFileName(path); + + // Ignore certain files + if (!string.IsNullOrEmpty(filename) && _alwaysIgnoreFiles.Contains(filename, StringComparer.OrdinalIgnoreCase)) + { + return; + } + + var tempIgnorePaths = _tempIgnoredPaths.Keys.ToList(); + + // If the parent of an ignored path has a change event, ignore that too + if (tempIgnorePaths.Any(i => + { + if (string.Equals(i, path, StringComparison.OrdinalIgnoreCase)) + { + Logger.Debug("Ignoring change to {0}", path); + return true; + } + + // Go up a level + var parent = Path.GetDirectoryName(i); + if (string.Equals(parent, path, StringComparison.OrdinalIgnoreCase)) + { + Logger.Debug("Ignoring change to {0}", path); + return true; + } + + // Go up another level + if (!string.IsNullOrEmpty(parent)) + { + parent = Path.GetDirectoryName(i); + if (string.Equals(parent, path, StringComparison.OrdinalIgnoreCase)) + { + Logger.Debug("Ignoring change to {0}", path); + return true; + } + } + + if (i.StartsWith(path, StringComparison.OrdinalIgnoreCase) || + path.StartsWith(i, StringComparison.OrdinalIgnoreCase)) + { + Logger.Debug("Ignoring change to {0}", path); + return true; + } + + return false; + + })) + { + return; + } + + // Avoid implicitly captured closure + var affectedPath = path; + _affectedPaths.AddOrUpdate(path, path, (key, oldValue) => affectedPath); + + lock (_timerLock) + { + if (_updateTimer == null) + { + _updateTimer = new Timer(TimerStopped, null, TimeSpan.FromSeconds(ConfigurationManager.Configuration.RealtimeWatcherDelay), TimeSpan.FromMilliseconds(-1)); + } + else + { + _updateTimer.Change(TimeSpan.FromSeconds(ConfigurationManager.Configuration.RealtimeWatcherDelay), TimeSpan.FromMilliseconds(-1)); + } + } + } + + /// + /// Timers the stopped. + /// + /// The state info. + private async void TimerStopped(object stateInfo) + { + Logger.Debug("Timer stopped."); + + DisposeTimer(); + + var paths = _affectedPaths.Keys.ToList(); + _affectedPaths.Clear(); + + await ProcessPathChanges(paths).ConfigureAwait(false); + } + + private void DisposeTimer() + { + lock (_timerLock) + { + if (_updateTimer != null) + { + _updateTimer.Dispose(); + _updateTimer = null; + } + } + } + + /// + /// Processes the path changes. + /// + /// The paths. + /// Task. + private async Task ProcessPathChanges(List paths) + { + var itemsToRefresh = paths.Select(Path.GetDirectoryName) + .Select(GetAffectedBaseItem) + .Where(item => item != null) + .Distinct() + .ToList(); + + foreach (var p in paths) Logger.Info(p + " reports change."); + + // If the root folder changed, run the library task so the user can see it + if (itemsToRefresh.Any(i => i is AggregateFolder)) + { + TaskManager.CancelIfRunningAndQueue(); + return; + } + + foreach (var item in itemsToRefresh) + { + Logger.Info(item.Name + " (" + item.Path + ") will be refreshed."); + + try + { + await item.ChangedExternally().ConfigureAwait(false); + } + catch (IOException ex) + { + // For now swallow and log. + // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable) + // Should we remove it from it's parent? + Logger.ErrorException("Error refreshing {0}", ex, item.Name); + } + catch (Exception ex) + { + Logger.ErrorException("Error refreshing {0}", ex, item.Name); + } + } + } + + /// + /// Gets the affected base item. + /// + /// The path. + /// BaseItem. + private BaseItem GetAffectedBaseItem(string path) + { + BaseItem item = null; + + while (item == null && !string.IsNullOrEmpty(path)) + { + item = LibraryManager.RootFolder.FindByPath(path); + + path = Path.GetDirectoryName(path); + } + + if (item != null) + { + // If the item has been deleted find the first valid parent that still exists + while (!Directory.Exists(item.Path) && !File.Exists(item.Path)) + { + item = item.Parent; + + if (item == null) + { + break; + } + } + } + + return item; + } + + /// + /// Stops this instance. + /// + public void Stop() + { + LibraryManager.ItemAdded -= LibraryManager_ItemAdded; + LibraryManager.ItemRemoved -= LibraryManager_ItemRemoved; + + foreach (var watcher in _fileSystemWatchers.Values.ToList()) + { + watcher.Changed -= watcher_Changed; + watcher.EnableRaisingEvents = false; + watcher.Dispose(); + } + + DisposeTimer(); + + _fileSystemWatchers.Clear(); + _affectedPaths.Clear(); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// 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) + { + Stop(); + } + } + } +} -- cgit v1.2.3 From 40b8300e8e72245c802938cd335a7ee962f8193f Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Tue, 28 Jan 2014 16:51:47 -0500 Subject: added api for external apps to report file system changes --- .../Library/LibraryStructureService.cs | 25 +++++++++++++ .../IO/LibraryMonitor.cs | 41 +++++++++++++++------- 2 files changed, 54 insertions(+), 12 deletions(-) (limited to 'MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs') diff --git a/MediaBrowser.Api/Library/LibraryStructureService.cs b/MediaBrowser.Api/Library/LibraryStructureService.cs index 56b0e01c3..d0d56c07b 100644 --- a/MediaBrowser.Api/Library/LibraryStructureService.cs +++ b/MediaBrowser.Api/Library/LibraryStructureService.cs @@ -167,6 +167,16 @@ namespace MediaBrowser.Api.Library public bool RefreshLibrary { get; set; } } + [Route("/Library/Changes/Path", "POST")] + public class ReportChangedPath : IReturnVoid + { + /// + /// Gets or sets the name. + /// + /// The name. + public string Path { get; set; } + } + /// /// Class LibraryStructureService /// @@ -214,6 +224,21 @@ namespace MediaBrowser.Api.Library _logger = logger; } + /// + /// Posts the specified request. + /// + /// The request. + /// Please supply a Path + public void Post(ReportChangedPath request) + { + if (string.IsNullOrEmpty(request.Path)) + { + throw new ArgumentException("Please supply a Path"); + } + + _libraryMonitor.ReportFileSystemChanged(request.Path); + } + /// /// Gets the specified request. /// diff --git a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs index e09e66765..bf90dc40e 100644 --- a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs +++ b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Common.ScheduledTasks; +using MediaBrowser.Common.IO; +using MediaBrowser.Common.ScheduledTasks; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -72,11 +73,21 @@ namespace MediaBrowser.Server.Implementations.IO public void ReportFileSystemChangeBeginning(string path) { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + TemporarilyIgnore(path); } public void ReportFileSystemChangeComplete(string path, bool refreshPath) { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + RemoveTempIgnore(path); if (refreshPath) @@ -100,6 +111,8 @@ namespace MediaBrowser.Server.Implementations.IO private ILibraryManager LibraryManager { get; set; } private IServerConfigurationManager ConfigurationManager { get; set; } + private IFileSystem _fileSystem; + /// /// Initializes a new instance of the class. /// @@ -350,6 +363,11 @@ namespace MediaBrowser.Server.Implementations.IO public void ReportFileSystemChanged(string path) { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + var filename = Path.GetFileName(path); // Ignore certain files @@ -369,30 +387,29 @@ namespace MediaBrowser.Server.Implementations.IO return true; } - // Go up a level - var parent = Path.GetDirectoryName(i); - if (string.Equals(parent, path, StringComparison.OrdinalIgnoreCase)) + if (_fileSystem.ContainsSubPath(i, path)) { Logger.Debug("Ignoring change to {0}", path); return true; } - // Go up another level + // Go up a level + var parent = Path.GetDirectoryName(i); if (!string.IsNullOrEmpty(parent)) { - parent = Path.GetDirectoryName(i); if (string.Equals(parent, path, StringComparison.OrdinalIgnoreCase)) { Logger.Debug("Ignoring change to {0}", path); return true; } - } - if (i.StartsWith(path, StringComparison.OrdinalIgnoreCase) || - path.StartsWith(i, StringComparison.OrdinalIgnoreCase)) - { - Logger.Debug("Ignoring change to {0}", path); - return true; + // Go up another level + parent = Path.GetDirectoryName(i); + if (string.Equals(parent, path, StringComparison.OrdinalIgnoreCase)) + { + Logger.Debug("Ignoring change to {0}", path); + return true; + } } return false; -- cgit v1.2.3 From e772b44ac73cf77b41072b7578a4014c6234ef1b Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Tue, 28 Jan 2014 20:46:04 -0500 Subject: fix library monitor reference --- MediaBrowser.Api/Library/LibraryStructureService.cs | 1 + MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs | 5 +++-- MediaBrowser.ServerApplication/ApplicationHost.cs | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) (limited to 'MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs') diff --git a/MediaBrowser.Api/Library/LibraryStructureService.cs b/MediaBrowser.Api/Library/LibraryStructureService.cs index d0d56c07b..8ea472da3 100644 --- a/MediaBrowser.Api/Library/LibraryStructureService.cs +++ b/MediaBrowser.Api/Library/LibraryStructureService.cs @@ -174,6 +174,7 @@ namespace MediaBrowser.Api.Library /// Gets or sets the name. /// /// The name. + [ApiMember(Name = "Path", Description = "The path that was changed.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] public string Path { get; set; } } diff --git a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs index bf90dc40e..22ea668f4 100644 --- a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs +++ b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs @@ -111,12 +111,12 @@ namespace MediaBrowser.Server.Implementations.IO private ILibraryManager LibraryManager { get; set; } private IServerConfigurationManager ConfigurationManager { get; set; } - private IFileSystem _fileSystem; + private readonly IFileSystem _fileSystem; /// /// Initializes a new instance of the class. /// - public LibraryMonitor(ILogManager logManager, ITaskManager taskManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager) + public LibraryMonitor(ILogManager logManager, ITaskManager taskManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager, IFileSystem fileSystem) { if (taskManager == null) { @@ -127,6 +127,7 @@ namespace MediaBrowser.Server.Implementations.IO TaskManager = taskManager; Logger = logManager.GetLogger(GetType().Name); ConfigurationManager = configurationManager; + _fileSystem = fileSystem; SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged; } diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index 0c8a6b923..bf652d8cb 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -276,7 +276,7 @@ namespace MediaBrowser.ServerApplication LibraryManager = new LibraryManager(Logger, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager); RegisterSingleInstance(LibraryManager); - LibraryMonitor = new LibraryMonitor(LogManager, TaskManager, LibraryManager, ServerConfigurationManager); + LibraryMonitor = new LibraryMonitor(LogManager, TaskManager, LibraryManager, ServerConfigurationManager, FileSystemManager); RegisterSingleInstance(LibraryMonitor); ProviderManager = new ProviderManager(HttpClient, ServerConfigurationManager, LibraryMonitor, LogManager, FileSystemManager, ItemRepository); -- cgit v1.2.3 From 81d5e9f8087227591b2be068e822342b17ef3d7a Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Wed, 29 Jan 2014 00:17:58 -0500 Subject: persist provider results --- MediaBrowser.Controller/Entities/BaseItem.cs | 7 +- .../MediaBrowser.Controller.csproj | 4 +- .../Persistence/IItemRepository.cs | 17 -- .../Providers/IMetadataProvider.cs | 28 +-- .../Providers/IProviderRepository.cs | 48 +++++ MediaBrowser.Controller/Providers/ItemId.cs | 35 ++++ .../Providers/MetadataStatus.cs | 122 ++++++++++++ .../Providers/ProviderResult.cs | 60 ------ .../GameGenres/GameGenreMetadataService.cs | 4 +- .../Genres/GenreMetadataService.cs | 4 +- .../Manager/ItemImageProvider.cs | 11 +- MediaBrowser.Providers/Manager/MetadataService.cs | 51 ++--- MediaBrowser.Providers/Manager/ProviderManager.cs | 12 +- .../MusicGenres/MusicGenreMetadataService.cs | 4 +- .../People/PersonMetadataService.cs | 4 +- .../Studios/StudioMetadataService.cs | 4 +- .../IO/LibraryMonitor.cs | 25 +-- .../Persistence/SqliteItemRepository.cs | 22 --- .../Persistence/SqliteProviderInfoRepository.cs | 212 ++++++++++++++++++--- MediaBrowser.ServerApplication/ApplicationHost.cs | 9 +- 20 files changed, 477 insertions(+), 206 deletions(-) create mode 100644 MediaBrowser.Controller/Providers/IProviderRepository.cs create mode 100644 MediaBrowser.Controller/Providers/ItemId.cs create mode 100644 MediaBrowser.Controller/Providers/MetadataStatus.cs delete mode 100644 MediaBrowser.Controller/Providers/ProviderResult.cs (limited to 'MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs') diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index a50a0f467..06ebe8905 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -364,11 +364,16 @@ namespace MediaBrowser.Controller.Entities } } + private string _forcedSortName; /// /// Gets or sets the name of the forced sort. /// /// The name of the forced sort. - public string ForcedSortName { get; set; } + public string ForcedSortName + { + get { return _forcedSortName; } + set { _forcedSortName = value; _sortName = null; } + } private string _sortName; /// diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 45297ef3d..ee8bb2761 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -146,13 +146,15 @@ + + - + diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 3affe48e7..3a5cb4e87 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -1,5 +1,4 @@ using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using System; using System.Collections.Generic; @@ -112,22 +111,6 @@ namespace MediaBrowser.Controller.Persistence /// The cancellation token. /// Task. Task SaveMediaStreams(Guid id, IEnumerable streams, CancellationToken cancellationToken); - - /// - /// Gets the provider history. - /// - /// The item identifier. - /// IEnumerable{BaseProviderInfo}. - IEnumerable GetProviderHistory(Guid itemId); - - /// - /// Saves the provider history. - /// - /// The identifier. - /// The history. - /// The cancellation token. - /// Task. - Task SaveProviderHistory(Guid id, IEnumerable history, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Providers/IMetadataProvider.cs b/MediaBrowser.Controller/Providers/IMetadataProvider.cs index 97249e26d..843ba263b 100644 --- a/MediaBrowser.Controller/Providers/IMetadataProvider.cs +++ b/MediaBrowser.Controller/Providers/IMetadataProvider.cs @@ -1,6 +1,4 @@ -using MediaBrowser.Model.Entities; -using System; -using System.Collections.Generic; +using System; using System.Threading; using System.Threading.Tasks; @@ -52,21 +50,14 @@ namespace MediaBrowser.Controller.Providers public interface IHasChangeMonitor { /// - /// Determines whether the specified date has changed. + /// Determines whether the specified item has changed. /// /// The item. /// The date. - /// true if the specified date has changed; otherwise, false. + /// true if the specified item has changed; otherwise, false. bool HasChanged(IHasMetadata item, DateTime date); } - public enum MetadataProviderType - { - Embedded = 0, - Local = 1, - Remote = 2 - } - public class MetadataResult where T : IHasMetadata { @@ -74,17 +65,4 @@ namespace MediaBrowser.Controller.Providers public T Item { get; set; } } - public class ItemId : IHasProviderIds - { - public string Name { get; set; } - public string MetadataLanguage { get; set; } - public string MetadataCountryCode { get; set; } - - public Dictionary ProviderIds { get; set; } - - public ItemId() - { - ProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - } } diff --git a/MediaBrowser.Controller/Providers/IProviderRepository.cs b/MediaBrowser.Controller/Providers/IProviderRepository.cs new file mode 100644 index 000000000..1c0ad2cd7 --- /dev/null +++ b/MediaBrowser.Controller/Providers/IProviderRepository.cs @@ -0,0 +1,48 @@ +using MediaBrowser.Controller.Persistence; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Providers +{ + public interface IProviderRepository : IRepository + { + /// + /// Gets the provider history. + /// + /// The item identifier. + /// IEnumerable{BaseProviderInfo}. + IEnumerable GetProviderHistory(Guid itemId); + + /// + /// Saves the provider history. + /// + /// The identifier. + /// The history. + /// The cancellation token. + /// Task. + Task SaveProviderHistory(Guid id, IEnumerable history, CancellationToken cancellationToken); + + /// + /// Gets the metadata status. + /// + /// The item identifier. + /// MetadataStatus. + MetadataStatus GetMetadataStatus(Guid itemId); + + /// + /// Saves the metadata status. + /// + /// The status. + /// The cancellation token. + /// Task. + Task SaveMetadataStatus(MetadataStatus status, CancellationToken cancellationToken); + + /// + /// Initializes this instance. + /// + /// Task. + Task Initialize(); + } +} diff --git a/MediaBrowser.Controller/Providers/ItemId.cs b/MediaBrowser.Controller/Providers/ItemId.cs new file mode 100644 index 000000000..1116eb8b5 --- /dev/null +++ b/MediaBrowser.Controller/Providers/ItemId.cs @@ -0,0 +1,35 @@ +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Providers +{ + public class ItemId : IHasProviderIds + { + /// + /// Gets or sets the name. + /// + /// The name. + public string Name { get; set; } + /// + /// Gets or sets the metadata language. + /// + /// The metadata language. + public string MetadataLanguage { get; set; } + /// + /// Gets or sets the metadata country code. + /// + /// The metadata country code. + public string MetadataCountryCode { get; set; } + /// + /// Gets or sets the provider ids. + /// + /// The provider ids. + public Dictionary ProviderIds { get; set; } + + public ItemId() + { + ProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + } +} diff --git a/MediaBrowser.Controller/Providers/MetadataStatus.cs b/MediaBrowser.Controller/Providers/MetadataStatus.cs new file mode 100644 index 000000000..834d8ec35 --- /dev/null +++ b/MediaBrowser.Controller/Providers/MetadataStatus.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Common.Extensions; + +namespace MediaBrowser.Controller.Providers +{ + public class MetadataStatus + { + /// + /// Gets or sets the item identifier. + /// + /// The item identifier. + public Guid ItemId { get; set; } + + /// + /// Gets or sets the date last metadata refresh. + /// + /// The date last metadata refresh. + public DateTime? DateLastMetadataRefresh { get; set; } + + /// + /// Gets or sets the date last images refresh. + /// + /// The date last images refresh. + public DateTime? DateLastImagesRefresh { get; set; } + + /// + /// Gets or sets the last result. + /// + /// The last result. + public ProviderRefreshStatus LastStatus { get; set; } + + /// + /// Gets or sets the last result error message. + /// + /// The last result error message. + public string LastErrorMessage { get; set; } + + /// + /// Gets or sets the providers refreshed. + /// + /// The providers refreshed. + public List MetadataProvidersRefreshed { get; set; } + public List ImageProvidersRefreshed { get; set; } + + public void AddStatus(ProviderRefreshStatus status, string errorMessage) + { + if (LastStatus != status) + { + IsDirty = true; + } + + if (string.IsNullOrEmpty(LastErrorMessage)) + { + LastErrorMessage = errorMessage; + } + if (LastStatus == ProviderRefreshStatus.Success) + { + LastStatus = status; + } + } + + public MetadataStatus() + { + LastStatus = ProviderRefreshStatus.Success; + + MetadataProvidersRefreshed = new List(); + ImageProvidersRefreshed = new List(); + } + + public bool IsDirty { get; private set; } + + public void SetDateLastMetadataRefresh(DateTime date) + { + if (date != (DateLastMetadataRefresh ?? DateTime.MinValue)) + { + IsDirty = true; + } + + DateLastMetadataRefresh = date; + } + + public void SetDateLastImagesRefresh(DateTime date) + { + if (date != (DateLastImagesRefresh ?? DateTime.MinValue)) + { + IsDirty = true; + } + + DateLastImagesRefresh = date; + } + + public void AddImageProvidersRefreshed(List providerIds) + { + var count = ImageProvidersRefreshed.Count; + + providerIds.AddRange(ImageProvidersRefreshed); + + ImageProvidersRefreshed = providerIds.Distinct().ToList(); + + if (ImageProvidersRefreshed.Count != count) + { + IsDirty = true; + } + } + + public void AddMetadataProvidersRefreshed(List providerIds) + { + var count = MetadataProvidersRefreshed.Count; + + providerIds.AddRange(MetadataProvidersRefreshed); + + MetadataProvidersRefreshed = providerIds.Distinct().ToList(); + + if (MetadataProvidersRefreshed.Count != count) + { + IsDirty = true; + } + } + } +} diff --git a/MediaBrowser.Controller/Providers/ProviderResult.cs b/MediaBrowser.Controller/Providers/ProviderResult.cs deleted file mode 100644 index 2486faeed..000000000 --- a/MediaBrowser.Controller/Providers/ProviderResult.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; - -namespace MediaBrowser.Controller.Providers -{ - public class ProviderResult - { - /// - /// Gets or sets the item identifier. - /// - /// The item identifier. - public Guid ItemId { get; set; } - - /// - /// Gets or sets a value indicating whether this instance has refreshed metadata. - /// - /// true if this instance has refreshed metadata; otherwise, false. - public bool HasRefreshedMetadata { get; set; } - - /// - /// Gets or sets a value indicating whether this instance has refreshed images. - /// - /// true if this instance has refreshed images; otherwise, false. - public bool HasRefreshedImages { get; set; } - - /// - /// Gets or sets the date last refreshed. - /// - /// The date last refreshed. - public DateTime DateLastRefreshed { get; set; } - - /// - /// Gets or sets the last result. - /// - /// The last result. - public ProviderRefreshStatus Status { get; set; } - - /// - /// Gets or sets the last result error message. - /// - /// The last result error message. - public string ErrorMessage { get; set; } - - public void AddStatus(ProviderRefreshStatus status, string errorMessage) - { - if (string.IsNullOrEmpty(ErrorMessage)) - { - ErrorMessage = errorMessage; - } - if (Status == ProviderRefreshStatus.Success) - { - Status = status; - } - } - - public ProviderResult() - { - Status = ProviderRefreshStatus.Success; - } - } -} diff --git a/MediaBrowser.Providers/GameGenres/GameGenreMetadataService.cs b/MediaBrowser.Providers/GameGenres/GameGenreMetadataService.cs index 7625cdd5a..389e2a275 100644 --- a/MediaBrowser.Providers/GameGenres/GameGenreMetadataService.cs +++ b/MediaBrowser.Providers/GameGenres/GameGenreMetadataService.cs @@ -15,8 +15,8 @@ namespace MediaBrowser.Providers.GameGenres { private readonly ILibraryManager _libraryManager; - public GameGenreMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager) + public GameGenreMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, providerRepo) { _libraryManager = libraryManager; } diff --git a/MediaBrowser.Providers/Genres/GenreMetadataService.cs b/MediaBrowser.Providers/Genres/GenreMetadataService.cs index 7a8ffa5d4..83253a190 100644 --- a/MediaBrowser.Providers/Genres/GenreMetadataService.cs +++ b/MediaBrowser.Providers/Genres/GenreMetadataService.cs @@ -15,8 +15,8 @@ namespace MediaBrowser.Providers.Genres { private readonly ILibraryManager _libraryManager; - public GenreMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager) + public GenreMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, providerRepo) { _libraryManager = libraryManager; } diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index 5358506f0..d455a08db 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -51,16 +52,24 @@ namespace MediaBrowser.Providers.Manager var providers = GetImageProviders(item, imageProviders).ToList(); + var providerIds = new List(); + foreach (var provider in providers.OfType()) { await RefreshFromProvider(item, provider, options, result, cancellationToken).ConfigureAwait(false); + + providerIds.Add(provider.GetType().FullName.GetMD5()); } foreach (var provider in providers.OfType()) { await RefreshFromProvider(item, provider, result, cancellationToken).ConfigureAwait(false); + + providerIds.Add(provider.GetType().FullName.GetMD5()); } + result.Providers = providerIds; + return result; } diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 5dde3098a..7916d7e86 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -18,16 +19,18 @@ namespace MediaBrowser.Providers.Manager protected readonly IServerConfigurationManager ServerConfigurationManager; protected readonly ILogger Logger; protected readonly IProviderManager ProviderManager; + private readonly IProviderRepository _providerRepo; private IMetadataProvider[] _providers = { }; private IImageProvider[] _imageProviders = { }; - protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager) + protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo) { ServerConfigurationManager = serverConfigurationManager; Logger = logger; ProviderManager = providerManager; + _providerRepo = providerRepo; } /// @@ -48,9 +51,9 @@ namespace MediaBrowser.Providers.Manager /// /// The result. /// Task. - protected Task SaveProviderResult(ProviderResult result) + protected Task SaveProviderResult(MetadataStatus result) { - return Task.FromResult(true); + return _providerRepo.SaveMetadataStatus(result, CancellationToken.None); } /// @@ -58,12 +61,9 @@ namespace MediaBrowser.Providers.Manager /// /// The item identifier. /// ProviderResult. - protected ProviderResult GetLastResult(Guid itemId) + protected MetadataStatus GetLastResult(Guid itemId) { - return new ProviderResult - { - ItemId = itemId - }; + return _providerRepo.GetMetadataStatus(itemId) ?? new MetadataStatus { ItemId = itemId }; } public async Task RefreshMetadata(IHasMetadata item, MetadataRefreshOptions options, CancellationToken cancellationToken) @@ -72,7 +72,9 @@ namespace MediaBrowser.Providers.Manager var updateType = ItemUpdateType.Unspecified; var lastResult = GetLastResult(item.Id); - var refreshResult = new ProviderResult { ItemId = item.Id }; + var refreshResult = lastResult; + refreshResult.LastErrorMessage = string.Empty; + refreshResult.LastStatus = ProviderRefreshStatus.Success; var imageProviders = GetImageProviders(item).ToList(); var itemImageProvider = new ItemImageProvider(Logger, ProviderManager, ServerConfigurationManager); @@ -97,7 +99,7 @@ namespace MediaBrowser.Providers.Manager // Next run metadata providers if (options.MetadataRefreshMode != MetadataRefreshMode.None) { - var providers = GetProviders(item, lastResult.HasRefreshedMetadata, options).ToList(); + var providers = GetProviders(item, lastResult.DateLastMetadataRefresh.HasValue, options).ToList(); if (providers.Count > 0) { @@ -105,22 +107,23 @@ namespace MediaBrowser.Providers.Manager updateType = updateType | result.UpdateType; refreshResult.AddStatus(result.Status, result.ErrorMessage); + refreshResult.SetDateLastMetadataRefresh(DateTime.UtcNow); + refreshResult.AddImageProvidersRefreshed(result.Providers); } - - refreshResult.HasRefreshedMetadata = true; } // Next run remote image providers, but only if local image providers didn't throw an exception if (!localImagesFailed) { - if ((options.ImageRefreshMode == MetadataRefreshMode.EnsureMetadata && !lastResult.HasRefreshedImages) || + if ((options.ImageRefreshMode == MetadataRefreshMode.EnsureMetadata && !lastResult.DateLastImagesRefresh.HasValue) || options.ImageRefreshMode == MetadataRefreshMode.FullRefresh) { - var imagesReult = await itemImageProvider.RefreshImages(itemOfType, imageProviders, options, cancellationToken).ConfigureAwait(false); + var result = await itemImageProvider.RefreshImages(itemOfType, imageProviders, options, cancellationToken).ConfigureAwait(false); - updateType = updateType | imagesReult.UpdateType; - refreshResult.AddStatus(imagesReult.Status, imagesReult.ErrorMessage); - refreshResult.HasRefreshedImages = true; + updateType = updateType | result.UpdateType; + refreshResult.AddStatus(result.Status, result.ErrorMessage); + refreshResult.SetDateLastImagesRefresh(DateTime.UtcNow); + refreshResult.AddImageProvidersRefreshed(result.Providers); } } @@ -137,9 +140,8 @@ namespace MediaBrowser.Providers.Manager await SaveItem(itemOfType, updateType, cancellationToken); } - if (providersHadChanges) + if (providersHadChanges || refreshResult.IsDirty) { - refreshResult.DateLastRefreshed = DateTime.UtcNow; await SaveProviderResult(refreshResult).ConfigureAwait(false); } } @@ -165,7 +167,7 @@ namespace MediaBrowser.Providers.Manager var currentItem = item; var providersWithChanges = providers.OfType() - .Where(i => i.HasChanged(currentItem, item.DateLastSaved)) + .Where(i => i.HasChanged(currentItem, currentItem.DateLastSaved)) .ToList(); // If local providers are the only ones with changes, then just run those @@ -219,7 +221,11 @@ namespace MediaBrowser.Providers.Manager protected virtual async Task RefreshWithProviders(TItemType item, MetadataRefreshOptions options, List providers, CancellationToken cancellationToken) { - var refreshResult = new RefreshResult { UpdateType = ItemUpdateType.Unspecified }; + var refreshResult = new RefreshResult + { + UpdateType = ItemUpdateType.Unspecified, + Providers = providers.Select(i => i.GetType().FullName.GetMD5()).ToList() + }; var temp = new TItemType(); @@ -347,5 +353,6 @@ namespace MediaBrowser.Providers.Manager public ItemUpdateType UpdateType { get; set; } public ProviderRefreshStatus Status { get; set; } public string ErrorMessage { get; set; } + public List Providers { get; set; } } } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index faad15669..3696bd02f 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -55,7 +55,7 @@ namespace MediaBrowser.Providers.Manager private readonly IFileSystem _fileSystem; - private readonly IItemRepository _itemRepo; + private readonly IProviderRepository _providerRepo; private IMetadataService[] _metadataServices = { }; @@ -67,15 +67,15 @@ namespace MediaBrowser.Providers.Manager /// The directory watchers. /// The log manager. /// The file system. - /// The item repo. - public ProviderManager(IHttpClient httpClient, IServerConfigurationManager configurationManager, ILibraryMonitor libraryMonitor, ILogManager logManager, IFileSystem fileSystem, IItemRepository itemRepo) + /// The provider repo. + public ProviderManager(IHttpClient httpClient, IServerConfigurationManager configurationManager, ILibraryMonitor libraryMonitor, ILogManager logManager, IFileSystem fileSystem, IProviderRepository providerRepo) { _logger = logManager.GetLogger("ProviderManager"); _httpClient = httpClient; ConfigurationManager = configurationManager; _libraryMonitor = libraryMonitor; _fileSystem = fileSystem; - _itemRepo = itemRepo; + _providerRepo = providerRepo; } /// @@ -136,7 +136,7 @@ namespace MediaBrowser.Providers.Manager var providerHistories = item.DateLastSaved == default(DateTime) ? new List() : - _itemRepo.GetProviderHistory(item.Id).ToList(); + _providerRepo.GetProviderHistory(item.Id).ToList(); // Run the normal providers sequentially in order of priority foreach (var provider in MetadataProviders) @@ -201,7 +201,7 @@ namespace MediaBrowser.Providers.Manager if (result.HasValue || force) { - await _itemRepo.SaveProviderHistory(item.Id, providerHistories, cancellationToken); + await _providerRepo.SaveProviderHistory(item.Id, providerHistories, cancellationToken); } return result; diff --git a/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs b/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs index bc4a99fc3..b88ca92bc 100644 --- a/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs +++ b/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs @@ -15,8 +15,8 @@ namespace MediaBrowser.Providers.MusicGenres { private readonly ILibraryManager _libraryManager; - public MusicGenreMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager) + public MusicGenreMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, providerRepo) { _libraryManager = libraryManager; } diff --git a/MediaBrowser.Providers/People/PersonMetadataService.cs b/MediaBrowser.Providers/People/PersonMetadataService.cs index 577158aa8..e04013934 100644 --- a/MediaBrowser.Providers/People/PersonMetadataService.cs +++ b/MediaBrowser.Providers/People/PersonMetadataService.cs @@ -15,8 +15,8 @@ namespace MediaBrowser.Providers.People { private readonly ILibraryManager _libraryManager; - public PersonMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager) + public PersonMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, providerRepo) { _libraryManager = libraryManager; } diff --git a/MediaBrowser.Providers/Studios/StudioMetadataService.cs b/MediaBrowser.Providers/Studios/StudioMetadataService.cs index 1a74d8317..1a35b94b3 100644 --- a/MediaBrowser.Providers/Studios/StudioMetadataService.cs +++ b/MediaBrowser.Providers/Studios/StudioMetadataService.cs @@ -15,8 +15,8 @@ namespace MediaBrowser.Providers.Studios { private readonly ILibraryManager _libraryManager; - public StudioMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager) + public StudioMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, providerRepo) { _libraryManager = libraryManager; } diff --git a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs index 22ea668f4..0716a3d83 100644 --- a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs +++ b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs @@ -56,21 +56,6 @@ namespace MediaBrowser.Server.Implementations.IO _tempIgnoredPaths[path] = path; } - /// - /// Removes the temp ignore. - /// - /// The path. - private async void RemoveTempIgnore(string path) - { - // This is an arbitraty amount of time, but delay it because file system writes often trigger events after RemoveTempIgnore has been called. - // Seeing long delays in some situations, especially over the network. - // Seeing delays up to 40 seconds, but not going to ignore changes for that long. - await Task.Delay(1500).ConfigureAwait(false); - - string val; - _tempIgnoredPaths.TryRemove(path, out val); - } - public void ReportFileSystemChangeBeginning(string path) { if (string.IsNullOrEmpty(path)) @@ -81,14 +66,20 @@ namespace MediaBrowser.Server.Implementations.IO TemporarilyIgnore(path); } - public void ReportFileSystemChangeComplete(string path, bool refreshPath) + public async void ReportFileSystemChangeComplete(string path, bool refreshPath) { if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException("path"); } - RemoveTempIgnore(path); + // This is an arbitraty amount of time, but delay it because file system writes often trigger events after RemoveTempIgnore has been called. + // Seeing long delays in some situations, especially over the network. + // Seeing delays up to 40 seconds, but not going to ignore changes for that long. + await Task.Delay(1500).ConfigureAwait(false); + + string val; + _tempIgnoredPaths.TryRemove(path, out val); if (refreshPath) { diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs index 200898a62..6b463bbdf 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs @@ -58,7 +58,6 @@ namespace MediaBrowser.Server.Implementations.Persistence private SqliteChapterRepository _chapterRepository; private SqliteMediaStreamsRepository _mediaStreamsRepository; - private SqliteProviderInfoRepository _providerInfoRepository; private IDbCommand _deleteChildrenCommand; private IDbCommand _saveChildrenCommand; @@ -99,10 +98,6 @@ namespace MediaBrowser.Server.Implementations.Persistence var mediaStreamsDbFile = Path.Combine(_appPaths.DataPath, "mediainfo.db"); var mediaStreamsConnection = SqliteExtensions.ConnectToDb(mediaStreamsDbFile, _logger).Result; _mediaStreamsRepository = new SqliteMediaStreamsRepository(mediaStreamsConnection, logManager); - - var providerInfosDbFile = Path.Combine(_appPaths.DataPath, "providerinfo.db"); - var providerInfoConnection = SqliteExtensions.ConnectToDb(providerInfosDbFile, _logger).Result; - _providerInfoRepository = new SqliteProviderInfoRepository(providerInfoConnection, logManager); } /// @@ -134,7 +129,6 @@ namespace MediaBrowser.Server.Implementations.Persistence PrepareStatements(); _mediaStreamsRepository.Initialize(); - _providerInfoRepository.Initialize(); _chapterRepository.Initialize(); _shrinkMemoryTimer = new SqliteShrinkMemoryTimer(_connection, _writeLock, _logger); @@ -436,12 +430,6 @@ namespace MediaBrowser.Server.Implementations.Persistence _mediaStreamsRepository.Dispose(); _mediaStreamsRepository = null; } - - if (_providerInfoRepository != null) - { - _providerInfoRepository.Dispose(); - _providerInfoRepository = null; - } } } catch (Exception ex) @@ -556,15 +544,5 @@ namespace MediaBrowser.Server.Implementations.Persistence { return _mediaStreamsRepository.SaveMediaStreams(id, streams, cancellationToken); } - - public IEnumerable GetProviderHistory(Guid itemId) - { - return _providerInfoRepository.GetBaseProviderInfos(itemId); - } - - public Task SaveProviderHistory(Guid id, IEnumerable history, CancellationToken cancellationToken) - { - return _providerInfoRepository.SaveProviderInfos(id, history, cancellationToken); - } } } \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteProviderInfoRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteProviderInfoRepository.cs index 9971c7460..8a82c062d 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteProviderInfoRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteProviderInfoRepository.cs @@ -1,4 +1,6 @@ -using MediaBrowser.Controller.Providers; +using System.IO; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; @@ -9,7 +11,7 @@ using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.Persistence { - class SqliteProviderInfoRepository + public class SqliteProviderInfoRepository : IProviderRepository { private IDbConnection _connection; @@ -17,32 +19,47 @@ namespace MediaBrowser.Server.Implementations.Persistence private IDbCommand _deleteInfosCommand; private IDbCommand _saveInfoCommand; + private IDbCommand _saveStatusCommand; + private readonly IApplicationPaths _appPaths; - public SqliteProviderInfoRepository(IDbConnection connection, ILogManager logManager) + public SqliteProviderInfoRepository(IApplicationPaths appPaths, ILogManager logManager) { - _connection = connection; - + _appPaths = appPaths; _logger = logManager.GetLogger(GetType().Name); } private SqliteShrinkMemoryTimer _shrinkMemoryTimer; - + + /// + /// Gets the name of the repository + /// + /// The name. + public string Name + { + get + { + return "SQLite"; + } + } + /// /// Opens the connection to the database /// /// Task. - public void Initialize() + public async Task Initialize() { - var createTableCommand - = "create table if not exists providerinfos "; + var dbFile = Path.Combine(_appPaths.DataPath, "providerinfo.db"); - createTableCommand += "(ItemId GUID, ProviderId GUID, ProviderVersion TEXT, FileStamp GUID, LastRefreshStatus TEXT, LastRefreshed datetime, PRIMARY KEY (ItemId, ProviderId))"; + _connection = await SqliteExtensions.ConnectToDb(dbFile, _logger).ConfigureAwait(false); string[] queries = { - createTableCommand, + "create table if not exists providerinfos (ItemId GUID, ProviderId GUID, ProviderVersion TEXT, FileStamp GUID, LastRefreshStatus TEXT, LastRefreshed datetime, PRIMARY KEY (ItemId, ProviderId))", "create index if not exists idx_providerinfos on providerinfos(ItemId, ProviderId)", + "create table if not exists MetadataStatus (ItemId GUID PRIMARY KEY, DateLastMetadataRefresh datetime, DateLastImagesRefresh datetime, LastStatus TEXT, LastErrorMessage TEXT, MetadataProvidersRefreshed TEXT, ImageProvidersRefreshed TEXT)", + "create index if not exists idx_MetadataStatus on MetadataStatus(ItemId)", + //pragmas "pragma temp_store = memory", @@ -56,7 +73,7 @@ namespace MediaBrowser.Server.Implementations.Persistence _shrinkMemoryTimer = new SqliteShrinkMemoryTimer(_connection, _writeLock, _logger); } - private static readonly string[] SaveColumns = + private static readonly string[] SaveHistoryColumns = { "ItemId", "ProviderId", @@ -66,7 +83,18 @@ namespace MediaBrowser.Server.Implementations.Persistence "LastRefreshed" }; - private readonly string[] _selectColumns = SaveColumns.Skip(1).ToArray(); + private readonly string[] _historySelectColumns = SaveHistoryColumns.Skip(1).ToArray(); + + private static readonly string[] StatusColumns = + { + "ItemId", + "DateLastMetadataRefresh", + "DateLastImagesRefresh", + "LastStatus", + "LastErrorMessage", + "MetadataProvidersRefreshed", + "ImageProvidersRefreshed" + }; /// /// The _write lock @@ -85,16 +113,27 @@ namespace MediaBrowser.Server.Implementations.Persistence _saveInfoCommand = _connection.CreateCommand(); _saveInfoCommand.CommandText = string.Format("replace into providerinfos ({0}) values ({1})", - string.Join(",", SaveColumns), - string.Join(",", SaveColumns.Select(i => "@" + i).ToArray())); + string.Join(",", SaveHistoryColumns), + string.Join(",", SaveHistoryColumns.Select(i => "@" + i).ToArray())); - foreach (var col in SaveColumns) + foreach (var col in SaveHistoryColumns) { _saveInfoCommand.Parameters.Add(_saveInfoCommand, "@" + col); } + + _saveStatusCommand = _connection.CreateCommand(); + + _saveStatusCommand.CommandText = string.Format("replace into MetadataStatus ({0}) values ({1})", + string.Join(",", StatusColumns), + string.Join(",", StatusColumns.Select(i => "@" + i).ToArray())); + + foreach (var col in StatusColumns) + { + _saveStatusCommand.Parameters.Add(_saveStatusCommand, "@" + col); + } } - public IEnumerable GetBaseProviderInfos(Guid itemId) + public IEnumerable GetProviderHistory(Guid itemId) { if (itemId == Guid.Empty) { @@ -103,7 +142,7 @@ namespace MediaBrowser.Server.Implementations.Persistence using (var cmd = _connection.CreateCommand()) { - var cmdText = "select " + string.Join(",", _selectColumns) + " from providerinfos where"; + var cmdText = "select " + string.Join(",", _historySelectColumns) + " from providerinfos where"; cmdText += " ItemId=@ItemId"; cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = itemId; @@ -121,10 +160,10 @@ namespace MediaBrowser.Server.Implementations.Persistence } /// - /// Gets the chapter. + /// Gets the base provider information. /// /// The reader. - /// ChapterInfo. + /// BaseProviderInfo. private BaseProviderInfo GetBaseProviderInfo(IDataReader reader) { var item = new BaseProviderInfo @@ -144,7 +183,7 @@ namespace MediaBrowser.Server.Implementations.Persistence return item; } - public async Task SaveProviderInfos(Guid id, IEnumerable infos, CancellationToken cancellationToken) + public async Task SaveProviderHistory(Guid id, IEnumerable infos, CancellationToken cancellationToken) { if (id == Guid.Empty) { @@ -166,7 +205,6 @@ namespace MediaBrowser.Server.Implementations.Persistence { transaction = _connection.BeginTransaction(); - // First delete chapters _deleteInfosCommand.GetParameter(0).Value = id; _deleteInfosCommand.Transaction = transaction; @@ -221,6 +259,136 @@ namespace MediaBrowser.Server.Implementations.Persistence } } + public MetadataStatus GetMetadataStatus(Guid itemId) + { + if (itemId == Guid.Empty) + { + throw new ArgumentNullException("itemId"); + } + + using (var cmd = _connection.CreateCommand()) + { + var cmdText = "select " + string.Join(",", StatusColumns) + " from MetadataStatus where"; + + cmdText += " ItemId=@ItemId"; + cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = itemId; + + cmd.CommandText = cmdText; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) + { + while (reader.Read()) + { + return GetStatus(reader); + } + + return null; + } + } + } + + private MetadataStatus GetStatus(IDataReader reader) + { + var result = new MetadataStatus + { + ItemId = reader.GetGuid(0) + }; + + if (!reader.IsDBNull(1)) + { + result.DateLastMetadataRefresh = reader.GetDateTime(1).ToUniversalTime(); + } + + if (!reader.IsDBNull(2)) + { + result.DateLastImagesRefresh = reader.GetDateTime(2).ToUniversalTime(); + } + + if (!reader.IsDBNull(3)) + { + result.LastStatus = (ProviderRefreshStatus)Enum.Parse(typeof(ProviderRefreshStatus), reader.GetString(3), true); + } + + if (!reader.IsDBNull(4)) + { + result.LastErrorMessage = reader.GetString(4); + } + + if (!reader.IsDBNull(5)) + { + result.MetadataProvidersRefreshed = reader.GetString(5).Split('|').Where(i => !string.IsNullOrEmpty(i)).Select(i => new Guid(i)).ToList(); + } + + if (!reader.IsDBNull(6)) + { + result.ImageProvidersRefreshed = reader.GetString(6).Split('|').Where(i => !string.IsNullOrEmpty(i)).Select(i => new Guid(i)).ToList(); + } + + return result; + } + + public async Task SaveMetadataStatus(MetadataStatus status, CancellationToken cancellationToken) + { + if (status == null) + { + throw new ArgumentNullException("status"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + IDbTransaction transaction = null; + + try + { + transaction = _connection.BeginTransaction(); + + _saveStatusCommand.GetParameter(0).Value = status.ItemId; + _saveStatusCommand.GetParameter(1).Value = status.DateLastMetadataRefresh; + _saveStatusCommand.GetParameter(2).Value = status.DateLastImagesRefresh; + _saveStatusCommand.GetParameter(3).Value = status.LastStatus.ToString(); + _saveStatusCommand.GetParameter(4).Value = status.LastErrorMessage; + _saveStatusCommand.GetParameter(5).Value = string.Join("|", status.MetadataProvidersRefreshed.ToArray()); + _saveStatusCommand.GetParameter(6).Value = string.Join("|", status.ImageProvidersRefreshed.ToArray()); + + _saveStatusCommand.Transaction = transaction; + + _saveStatusCommand.ExecuteNonQuery(); + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + _logger.ErrorException("Failed to save provider info:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + + _writeLock.Release(); + } + } + /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index bf652d8cb..4b15ca8d0 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -14,7 +14,6 @@ using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.FileOrganization; -using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Localization; @@ -173,6 +172,7 @@ namespace MediaBrowser.ServerApplication internal IItemRepository ItemRepository { get; set; } private INotificationsRepository NotificationsRepository { get; set; } private IFileOrganizationRepository FileOrganizationRepository { get; set; } + private IProviderRepository ProviderRepository { get; set; } /// /// Initializes a new instance of the class. @@ -267,6 +267,9 @@ namespace MediaBrowser.ServerApplication ItemRepository = new SqliteItemRepository(ApplicationPaths, JsonSerializer, LogManager); RegisterSingleInstance(ItemRepository); + ProviderRepository = new SqliteProviderInfoRepository(ApplicationPaths, LogManager); + RegisterSingleInstance(ProviderRepository); + FileOrganizationRepository = await GetFileOrganizationRepository().ConfigureAwait(false); RegisterSingleInstance(FileOrganizationRepository); @@ -279,7 +282,7 @@ namespace MediaBrowser.ServerApplication LibraryMonitor = new LibraryMonitor(LogManager, TaskManager, LibraryManager, ServerConfigurationManager, FileSystemManager); RegisterSingleInstance(LibraryMonitor); - ProviderManager = new ProviderManager(HttpClient, ServerConfigurationManager, LibraryMonitor, LogManager, FileSystemManager, ItemRepository); + ProviderManager = new ProviderManager(HttpClient, ServerConfigurationManager, LibraryMonitor, LogManager, FileSystemManager, ProviderRepository); RegisterSingleInstance(ProviderManager); RegisterSingleInstance(() => new SearchEngine(LogManager, LibraryManager, UserManager)); @@ -427,6 +430,8 @@ namespace MediaBrowser.ServerApplication { await ItemRepository.Initialize().ConfigureAwait(false); + await ProviderRepository.Initialize().ConfigureAwait(false); + ((LibraryManager)LibraryManager).ItemRepository = ItemRepository; } -- cgit v1.2.3