using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Controller.Providers { /// /// Class BaseMetadataProvider /// public abstract class BaseMetadataProvider { /// /// Gets the logger. /// /// The logger. protected ILogger Logger { get; set; } protected ILogManager LogManager { get; set; } /// /// Gets the configuration manager. /// /// The configuration manager. protected IServerConfigurationManager ConfigurationManager { get; private set; } /// /// The _id /// protected readonly Guid Id; /// /// The true task result /// protected static readonly Task TrueTaskResult = Task.FromResult(true); protected static readonly Task FalseTaskResult = Task.FromResult(false); protected static readonly SemaphoreSlim XmlParsingResourcePool = new SemaphoreSlim(5, 5); /// /// Supportses the specified item. /// /// The item. /// true if XXXX, false otherwise public abstract bool Supports(BaseItem item); /// /// Gets a value indicating whether [requires internet]. /// /// true if [requires internet]; otherwise, false. public virtual bool RequiresInternet { get { return false; } } /// /// Gets the provider version. /// /// The provider version. protected virtual string ProviderVersion { get { return null; } } public virtual ItemUpdateType ItemUpdateType { get { return RequiresInternet ? ItemUpdateType.MetadataDownload : ItemUpdateType.MetadataImport; } } /// /// Gets a value indicating whether [refresh on version change]. /// /// true if [refresh on version change]; otherwise, false. protected virtual bool RefreshOnVersionChange { get { return false; } } /// /// Determines if this provider is relatively slow and, therefore, should be skipped /// in certain instances. Default is whether or not it requires internet. Can be overridden /// for explicit designation. /// /// true if this instance is slow; otherwise, false. public virtual bool IsSlow { get { return RequiresInternet; } } /// /// Initializes a new instance of the class. /// protected BaseMetadataProvider(ILogManager logManager, IServerConfigurationManager configurationManager) { Logger = logManager.GetLogger(GetType().Name); LogManager = logManager; ConfigurationManager = configurationManager; Id = GetType().FullName.GetMD5(); Initialize(); } /// /// Initializes this instance. /// protected virtual void Initialize() { } /// /// Sets the persisted last refresh date on the item for this provider. /// /// The item. /// The value. /// The provider version. /// The status. /// item public virtual void SetLastRefreshed(BaseItem item, DateTime value, string providerVersion, ProviderRefreshStatus status = ProviderRefreshStatus.Success) { if (item == null) { throw new ArgumentNullException("item"); } BaseProviderInfo data; if (!item.ProviderData.TryGetValue(Id, out data)) { data = new BaseProviderInfo(); } data.LastRefreshed = value; data.LastRefreshStatus = status; data.ProviderVersion = providerVersion; // Save the file system stamp for future comparisons if (RefreshOnFileSystemStampChange && item.LocationType == LocationType.FileSystem) { try { data.FileStamp = GetCurrentFileSystemStamp(item); } catch (IOException ex) { Logger.ErrorException("Error getting file stamp for {0}", ex, item.Path); } } item.ProviderData[Id] = data; } /// /// Sets the last refreshed. /// /// The item. /// The value. /// The status. public void SetLastRefreshed(BaseItem item, DateTime value, ProviderRefreshStatus status = ProviderRefreshStatus.Success) { SetLastRefreshed(item, value, ProviderVersion, status); } /// /// Returns whether or not this provider should be re-fetched. Default functionality can /// compare a provided date with a last refresh time. This can be overridden for more complex /// determinations. /// /// The item. /// true if XXXX, false otherwise /// public bool NeedsRefresh(BaseItem item) { if (item == null) { throw new ArgumentNullException(); } BaseProviderInfo data; if (!item.ProviderData.TryGetValue(Id, out data)) { data = new BaseProviderInfo(); } return NeedsRefreshInternal(item, data); } /// /// Gets a value indicating whether [enforce dont fetch metadata]. /// /// true if [enforce dont fetch metadata]; otherwise, false. public virtual bool EnforceDontFetchMetadata { get { return true; } } /// /// Needses the refresh internal. /// /// The item. /// The provider info. /// true if XXXX, false otherwise /// protected virtual bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) { if (item == null) { throw new ArgumentNullException("item"); } if (providerInfo == null) { throw new ArgumentNullException("providerInfo"); } if (NeedsRefreshBasedOnCompareDate(item, providerInfo)) { return true; } if (RefreshOnFileSystemStampChange && item.LocationType == LocationType.FileSystem && HasFileSystemStampChanged(item, providerInfo)) { return true; } if (RefreshOnVersionChange && !String.Equals(ProviderVersion, providerInfo.ProviderVersion)) { return true; } if (providerInfo.LastRefreshStatus != ProviderRefreshStatus.Success) { return true; } return false; } /// /// Needses the refresh based on compare date. /// /// The item. /// The provider info. /// true if XXXX, false otherwise protected virtual bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo) { return CompareDate(item) > providerInfo.LastRefreshed; } /// /// Determines if the item's file system stamp has changed from the last time the provider refreshed /// /// The item. /// The provider info. /// true if [has file system stamp changed] [the specified item]; otherwise, false. protected bool HasFileSystemStampChanged(BaseItem item, BaseProviderInfo providerInfo) { return GetCurrentFileSystemStamp(item) != providerInfo.FileStamp; } /// /// Override this to return the date that should be compared to the last refresh date /// to determine if this provider should be re-fetched. /// /// The item. /// DateTime. protected virtual DateTime CompareDate(BaseItem item) { return DateTime.MinValue.AddMinutes(1); // want this to be greater than mindate so new items will refresh } /// /// Fetches metadata and returns true or false indicating if any work that requires persistence was done /// /// The item. /// if set to true [force]. /// The cancellation token. /// Task{System.Boolean}. /// public abstract Task FetchAsync(BaseItem item, bool force, CancellationToken cancellationToken); /// /// Gets the priority. /// /// The priority. public abstract MetadataProviderPriority Priority { get; } /// /// Returns true or false indicating if the provider should refresh when the contents of it's directory changes /// /// true if [refresh on file system stamp change]; otherwise, false. protected virtual bool RefreshOnFileSystemStampChange { get { return false; } } protected virtual string[] FilestampExtensions { get { return new string[] { }; } } /// /// Determines if the parent's file system stamp should be used for comparison /// /// The item. /// true if XXXX, false otherwise protected virtual bool UseParentFileSystemStamp(BaseItem item) { // True when the current item is just a file return !item.ResolveArgs.IsDirectory; } /// /// Gets the item's current file system stamp /// /// The item. /// Guid. private Guid GetCurrentFileSystemStamp(BaseItem item) { if (UseParentFileSystemStamp(item) && item.Parent != null) { return GetFileSystemStamp(item.Parent); } return GetFileSystemStamp(item); } private Dictionary _fileStampExtensionsDictionary; private Dictionary FileStampExtensionsDictionary { get { return _fileStampExtensionsDictionary ?? (_fileStampExtensionsDictionary = FilestampExtensions.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase)); } } /// /// Gets the file system stamp. /// /// The item. /// Guid. private Guid GetFileSystemStamp(BaseItem item) { // If there's no path or the item is a file, there's nothing to do if (item.LocationType != LocationType.FileSystem) { return Guid.Empty; } ItemResolveArgs resolveArgs; try { resolveArgs = item.ResolveArgs; } catch (IOException ex) { Logger.ErrorException("Error determining if path is directory: {0}", ex, item.Path); throw; } if (!resolveArgs.IsDirectory) { return Guid.Empty; } var sb = new StringBuilder(); var extensions = FileStampExtensionsDictionary; var numExtensions = FilestampExtensions.Length; // Record the name of each file // Need to sort these because accoring to msdn docs, our i/o methods are not guaranteed in any order AddFiles(sb, resolveArgs.FileSystemChildren, extensions, numExtensions); AddFiles(sb, resolveArgs.MetadataFiles, extensions, numExtensions); return sb.ToString().GetMD5(); } private static readonly Dictionary FoldersToMonitor = new[] { "extrafanart", "extrathumbs" } .ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); /// /// Adds the files. /// /// The sb. /// The files. /// The extensions. /// The num extensions. private void AddFiles(StringBuilder sb, IEnumerable files, Dictionary extensions, int numExtensions) { foreach (var file in files .OrderBy(f => f.Name)) { try { if ((file.Attributes & FileAttributes.Directory) == FileAttributes.Directory) { if (FoldersToMonitor.ContainsKey(file.Name)) { sb.Append(file.Name); var children = ((DirectoryInfo) file).EnumerateFiles("*", SearchOption.TopDirectoryOnly).ToList(); AddFiles(sb, children, extensions, numExtensions); } } // It's a file else if (numExtensions == 0 || extensions.ContainsKey(file.Extension)) { sb.Append(file.Name); } } catch (IOException ex) { Logger.ErrorException("Error accessing file attributes for {0}", ex, file.FullName); } } } } }