using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.IO; using MediaBrowser.Common.Events; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Logging; using MediaBrowser.Model.System; using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Threading; namespace Emby.Server.Implementations.IO { public class FileRefresher : IDisposable { private ILogger Logger { get; set; } private ITaskManager TaskManager { get; set; } private ILibraryManager LibraryManager { get; set; } private IServerConfigurationManager ConfigurationManager { get; set; } private readonly IFileSystem _fileSystem; private readonly List _affectedPaths = new List(); private ITimer _timer; private readonly ITimerFactory _timerFactory; private readonly object _timerLock = new object(); public string Path { get; private set; } public event EventHandler Completed; private readonly IEnvironmentInfo _environmentInfo; private readonly ILibraryManager _libraryManager; public FileRefresher(string path, IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ITaskManager taskManager, ILogger logger, ITimerFactory timerFactory, IEnvironmentInfo environmentInfo, ILibraryManager libraryManager1) { logger.Debug("New file refresher created for {0}", path); Path = path; _fileSystem = fileSystem; ConfigurationManager = configurationManager; LibraryManager = libraryManager; TaskManager = taskManager; Logger = logger; _timerFactory = timerFactory; _environmentInfo = environmentInfo; _libraryManager = libraryManager1; AddPath(path); } private void AddAffectedPath(string path) { if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentNullException("path"); } if (!_affectedPaths.Contains(path, StringComparer.Ordinal)) { _affectedPaths.Add(path); } } public void AddPath(string path) { if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentNullException("path"); } lock (_timerLock) { AddAffectedPath(path); } RestartTimer(); } public void RestartTimer() { if (_disposed) { return; } lock (_timerLock) { if (_disposed) { return; } if (_timer == null) { _timer = _timerFactory.Create(OnTimerCallback, null, TimeSpan.FromSeconds(ConfigurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1)); } else { _timer.Change(TimeSpan.FromSeconds(ConfigurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1)); } } } public void ResetPath(string path, string affectedFile) { lock (_timerLock) { Logger.Debug("Resetting file refresher from {0} to {1}", Path, path); Path = path; AddAffectedPath(path); if (!string.IsNullOrWhiteSpace(affectedFile)) { AddAffectedPath(affectedFile); } } RestartTimer(); } private void OnTimerCallback(object state) { List paths; lock (_timerLock) { paths = _affectedPaths.ToList(); } Logger.Debug("Timer stopped."); DisposeTimer(); EventHelper.FireEventIfNotNull(Completed, this, EventArgs.Empty, Logger); try { ProcessPathChanges(paths.ToList()); } catch (Exception ex) { Logger.ErrorException("Error processing directory changes", ex); } } private void ProcessPathChanges(List paths) { var itemsToRefresh = paths .Distinct(StringComparer.OrdinalIgnoreCase) .Select(GetAffectedBaseItem) .Where(item => item != null) .DistinctBy(i => i.Id) .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)) { LibraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None); return; } foreach (var item in itemsToRefresh) { Logger.Info(item.Name + " (" + item.Path + ") will be refreshed."); try { item.ChangedExternally(); } 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.FindByPath(path, null); path = _fileSystem.GetDirectoryName(path); } if (item != null) { // If the item has been deleted find the first valid parent that still exists while (!_fileSystem.DirectoryExists(item.Path) && !_fileSystem.FileExists(item.Path)) { item = item.GetParent(); if (item == null) { break; } } } return item; } private void DisposeTimer() { lock (_timerLock) { if (_timer != null) { _timer.Dispose(); _timer = null; } } } private bool _disposed; public void Dispose() { _disposed = true; DisposeTimer(); GC.SuppressFinalize(this); } } }