using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; 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.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); } /// /// 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 (CompareDate(item) > providerInfo.LastRefreshed) { return true; } if (RefreshOnFileSystemStampChange && item.LocationType == LocationType.FileSystem && HasFileSystemStampChanged(item, providerInfo)) { return true; } if (RefreshOnVersionChange && !String.Equals(ProviderVersion, providerInfo.ProviderVersion)) { return true; } if (RequiresInternet && DateTime.UtcNow > (providerInfo.LastRefreshed.AddDays(ConfigurationManager.Configuration.MetadataRefreshDays))) { return true; } if (providerInfo.LastRefreshStatus != ProviderRefreshStatus.Success) { return true; } return false; } /// /// 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); } /// /// 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 extensionsList = FilestampExtensions; var extensions = extensionsList.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); // 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 foreach (var file in resolveArgs.FileSystemChildren .Where(i => IncludeInFileStamp(i, extensions, extensionsList.Length)) .OrderBy(f => f.Name)) { sb.Append(file.Name); } foreach (var file in resolveArgs.MetadataFiles .Where(i => IncludeInFileStamp(i, extensions, extensionsList.Length)) .OrderBy(f => f.Name)) { sb.Append(file.Name); } return sb.ToString().GetMD5(); } /// /// Includes the in file stamp. /// /// The file. /// The extensions. /// The num extensions. /// true if XXXX, false otherwise private bool IncludeInFileStamp(FileSystemInfo file, Dictionary extensions, int numExtensions) { try { if ((file.Attributes & FileAttributes.Directory) == FileAttributes.Directory) { return false; } return numExtensions == 0 || extensions.ContainsKey(file.Extension); } catch (IOException ex) { Logger.ErrorException("Error accessing file attributes for {0}", ex, file.FullName); return false; } } } }