using MediaBrowser.Common.Extensions; using MediaBrowser.Common.IO; using MediaBrowser.Common.Progress; using MediaBrowser.Common.ScheduledTasks; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Naming.Audio; using MediaBrowser.Naming.Common; using MediaBrowser.Naming.IO; using MediaBrowser.Naming.TV; using MediaBrowser.Naming.Video; using MediaBrowser.Server.Implementations.Library.Validators; using MediaBrowser.Server.Implementations.Logging; using MediaBrowser.Server.Implementations.ScheduledTasks; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using SortOrder = MediaBrowser.Model.Entities.SortOrder; namespace MediaBrowser.Server.Implementations.Library { /// /// Class LibraryManager /// public class LibraryManager : ILibraryManager { /// /// Gets or sets the postscan tasks. /// /// The postscan tasks. private ILibraryPostScanTask[] PostscanTasks { get; set; } /// /// Gets the intro providers. /// /// The intro providers. private IIntroProvider[] IntroProviders { get; set; } /// /// Gets the list of entity resolution ignore rules /// /// The entity resolution ignore rules. private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } /// /// Gets the list of BasePluginFolders added by plugins /// /// The plugin folders. private IVirtualFolderCreator[] PluginFolderCreators { get; set; } /// /// Gets the list of currently registered entity resolvers /// /// The entity resolvers enumerable. private IItemResolver[] EntityResolvers { get; set; } private IMultiItemResolver[] MultiItemResolvers { get; set; } /// /// Gets or sets the comparers. /// /// The comparers. private IBaseItemComparer[] Comparers { get; set; } /// /// Gets the active item repository /// /// The item repository. public IItemRepository ItemRepository { get; set; } /// /// Occurs when [item added]. /// public event EventHandler ItemAdded; /// /// Occurs when [item updated]. /// public event EventHandler ItemUpdated; /// /// Occurs when [item removed]. /// public event EventHandler ItemRemoved; /// /// The _logger /// private readonly ILogger _logger; /// /// The _task manager /// private readonly ITaskManager _taskManager; /// /// The _user manager /// private readonly IUserManager _userManager; /// /// The _user data repository /// private readonly IUserDataManager _userDataRepository; /// /// Gets or sets the configuration manager. /// /// The configuration manager. private IServerConfigurationManager ConfigurationManager { get; set; } /// /// A collection of items that may be referenced from multiple physical places in the library /// (typically, multiple user roots). We store them here and be sure they all reference a /// single instance. /// /// The by reference items. private ConcurrentDictionary ByReferenceItems { get; set; } private readonly Func _libraryMonitorFactory; private readonly Func _providerManagerFactory; /// /// The _library items cache /// private readonly ConcurrentDictionary _libraryItemsCache; /// /// Gets the library items cache. /// /// The library items cache. private ConcurrentDictionary LibraryItemsCache { get { return _libraryItemsCache; } } private readonly IFileSystem _fileSystem; /// /// Initializes a new instance of the class. /// /// The logger. /// The task manager. /// The user manager. /// The configuration manager. /// The user data repository. public LibraryManager(ILogger logger, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, IUserDataManager userDataRepository, Func libraryMonitorFactory, IFileSystem fileSystem, Func providerManagerFactory) { _logger = logger; _taskManager = taskManager; _userManager = userManager; ConfigurationManager = configurationManager; _userDataRepository = userDataRepository; _libraryMonitorFactory = libraryMonitorFactory; _fileSystem = fileSystem; _providerManagerFactory = providerManagerFactory; ByReferenceItems = new ConcurrentDictionary(); _libraryItemsCache = new ConcurrentDictionary(); ConfigurationManager.ConfigurationUpdated += ConfigurationUpdated; RecordConfigurationValues(configurationManager.Configuration); } /// /// Adds the parts. /// /// The rules. /// The plugin folders. /// The resolvers. /// The intro providers. /// The item comparers. /// The postscan tasks. public void AddParts(IEnumerable rules, IEnumerable pluginFolders, IEnumerable resolvers, IEnumerable introProviders, IEnumerable itemComparers, IEnumerable postscanTasks) { EntityResolutionIgnoreRules = rules.ToArray(); PluginFolderCreators = pluginFolders.ToArray(); EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray(); MultiItemResolvers = EntityResolvers.OfType().ToArray(); IntroProviders = introProviders.ToArray(); Comparers = itemComparers.ToArray(); PostscanTasks = postscanTasks.OrderBy(i => { var hasOrder = i as IHasOrder; return hasOrder == null ? 0 : hasOrder.Order; }).ToArray(); } /// /// The _root folder /// private AggregateFolder _rootFolder; /// /// The _root folder sync lock /// private readonly object _rootFolderSyncLock = new object(); /// /// Gets the root folder. /// /// The root folder. public AggregateFolder RootFolder { get { if (_rootFolder == null) { lock (_rootFolderSyncLock) { if (_rootFolder == null) { _rootFolder = CreateRootFolder(); } } } return _rootFolder; } } /// /// The _items by name path /// private string _itemsByNamePath; /// /// The _season zero display name /// private string _seasonZeroDisplayName; private bool _wizardCompleted; /// /// Records the configuration values. /// /// The configuration. private void RecordConfigurationValues(ServerConfiguration configuration) { _seasonZeroDisplayName = configuration.SeasonZeroDisplayName; _itemsByNamePath = ConfigurationManager.ApplicationPaths.ItemsByNamePath; _wizardCompleted = configuration.IsStartupWizardCompleted; } /// /// Configurations the updated. /// /// The sender. /// The instance containing the event data. void ConfigurationUpdated(object sender, EventArgs e) { var config = ConfigurationManager.Configuration; var ibnPathChanged = !string.Equals(_itemsByNamePath, ConfigurationManager.ApplicationPaths.ItemsByNamePath, StringComparison.Ordinal); if (ibnPathChanged) { RemoveItemsByNameFromCache(); } var newSeasonZeroName = ConfigurationManager.Configuration.SeasonZeroDisplayName; var seasonZeroNameChanged = !string.Equals(_seasonZeroDisplayName, newSeasonZeroName, StringComparison.Ordinal); var wizardChanged = config.IsStartupWizardCompleted != _wizardCompleted; RecordConfigurationValues(config); Task.Run(async () => { if (seasonZeroNameChanged) { await UpdateSeasonZeroNames(newSeasonZeroName, CancellationToken.None).ConfigureAwait(false); } if (seasonZeroNameChanged || ibnPathChanged || wizardChanged) { _taskManager.CancelIfRunningAndQueue(); } }); } private void RemoveItemsByNameFromCache() { RemoveItemsFromCache(i => i is Person); RemoveItemsFromCache(i => i is Year); RemoveItemsFromCache(i => i is Genre); RemoveItemsFromCache(i => i is MusicGenre); RemoveItemsFromCache(i => i is GameGenre); RemoveItemsFromCache(i => i is Studio); RemoveItemsFromCache(i => { var artist = i as MusicArtist; return artist != null && artist.IsAccessedByName; }); } private void RemoveItemsFromCache(Func remove) { var items = _libraryItemsCache.ToList().Where(i => remove(i.Value)).ToList(); foreach (var item in items) { BaseItem value; _libraryItemsCache.TryRemove(item.Key, out value); } } /// /// Updates the season zero names. /// /// The new name. /// The cancellation token. /// Task. private async Task UpdateSeasonZeroNames(string newName, CancellationToken cancellationToken) { var seasons = RootFolder.GetRecursiveChildren(i => i is Season) .Cast() .Where(i => i.IndexNumber.HasValue && i.IndexNumber.Value == 0 && !string.Equals(i.Name, newName, StringComparison.Ordinal)) .ToList(); foreach (var season in seasons) { season.Name = newName; try { await UpdateItem(season, ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { _logger.ErrorException("Error saving {0}", ex, season.Path); } } } /// /// Updates the item in library cache. /// /// The item. private void UpdateItemInLibraryCache(BaseItem item) { RegisterItem(item); } public void RegisterItem(BaseItem item) { if (item == null) { throw new ArgumentNullException("item"); } RegisterItem(item.Id, item); } private void RegisterItem(Guid id, BaseItem item) { LibraryItemsCache.AddOrUpdate(id, item, delegate { return item; }); } public async Task DeleteItem(BaseItem item, DeleteOptions options) { _logger.Debug("Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", item.GetType().Name, item.Name, item.Path ?? string.Empty, item.Id); var parent = item.Parent; var locationType = item.LocationType; var children = item.IsFolder ? ((Folder)item).GetRecursiveChildren().ToList() : new List(); foreach (var metadataPath in GetMetadataPaths(item, children)) { _logger.Debug("Deleting path {0}", metadataPath); try { _fileSystem.DeleteDirectory(metadataPath, true); } catch (DirectoryNotFoundException) { } catch (Exception ex) { _logger.ErrorException("Error deleting {0}", ex, metadataPath); } } if (options.DeleteFileLocation && locationType != LocationType.Remote && locationType != LocationType.Virtual) { foreach (var path in item.GetDeletePaths().ToList()) { if (Directory.Exists(path)) { _logger.Debug("Deleting path {0}", path); _fileSystem.DeleteDirectory(path, true); } else if (File.Exists(path)) { _logger.Debug("Deleting path {0}", path); _fileSystem.DeleteFile(path); } } if (parent != null) { await parent.ValidateChildren(new Progress(), CancellationToken.None) .ConfigureAwait(false); } } else if (parent != null) { await parent.RemoveChild(item, CancellationToken.None).ConfigureAwait(false); } await ItemRepository.DeleteItem(item.Id, CancellationToken.None).ConfigureAwait(false); foreach (var child in children) { await ItemRepository.DeleteItem(child.Id, CancellationToken.None).ConfigureAwait(false); } BaseItem removed; _libraryItemsCache.TryRemove(item.Id, out removed); ReportItemRemoved(item); } private IEnumerable GetMetadataPaths(BaseItem item, IEnumerable children) { var list = new List { item.GetInternalMetadataPath() }; list.AddRange(children.Select(i => i.GetInternalMetadataPath())); return list; } /// /// Resolves the item. /// /// The args. /// BaseItem. private BaseItem ResolveItem(ItemResolveArgs args) { var item = EntityResolvers.Select(r => Resolve(args, r)) .FirstOrDefault(i => i != null); if (item != null) { ResolverHelper.SetInitialItemValues(item, args, _fileSystem, this); } return item; } private BaseItem Resolve(ItemResolveArgs args, IItemResolver resolver) { try { return resolver.ResolvePath(args); } catch (Exception ex) { _logger.ErrorException("Error in {0} resolving {1}", ex, resolver.GetType().Name, args.Path); return null; } } public Guid GetNewItemId(string key, Type type) { if (string.IsNullOrWhiteSpace(key)) { throw new ArgumentNullException("key"); } if (type == null) { throw new ArgumentNullException("type"); } if (ConfigurationManager.Configuration.EnableLocalizedGuids && key.StartsWith(ConfigurationManager.ApplicationPaths.ProgramDataPath)) { // Try to normalize paths located underneath program-data in an attempt to make them more portable key = key.Substring(ConfigurationManager.ApplicationPaths.ProgramDataPath.Length) .TrimStart(new[] { '/', '\\' }) .Replace("/", "\\"); } key = type.FullName + key.ToLower(); return key.GetMD5(); } public IEnumerable ReplaceVideosWithPrimaryVersions(IEnumerable items) { if (items == null) { throw new ArgumentNullException("items"); } var dict = new Dictionary(); foreach (var item in items) { var video = item as Video; if (video != null) { if (video.PrimaryVersionId.HasValue) { var primary = GetItemById(video.PrimaryVersionId.Value) as Video; if (primary != null) { dict[primary.Id] = primary; continue; } } } dict[item.Id] = item; } return dict.Values; } /// /// Ensure supplied item has only one instance throughout /// /// The item. /// The proper instance to the item public BaseItem GetOrAddByReferenceItem(BaseItem item) { // Add this item to our list if not there already if (!ByReferenceItems.TryAdd(item.Id, item)) { // Already there - return the existing reference item = ByReferenceItems[item.Id]; } return item; } public BaseItem ResolvePath(FileSystemInfo fileInfo, Folder parent = null) { return ResolvePath(fileInfo, new DirectoryService(_logger), parent); } private BaseItem ResolvePath(FileSystemInfo fileInfo, IDirectoryService directoryService, Folder parent = null, string collectionType = null) { if (fileInfo == null) { throw new ArgumentNullException("fileInfo"); } var fullPath = fileInfo.FullName; if (string.IsNullOrWhiteSpace(collectionType) && parent != null) { collectionType = GetContentTypeOverride(fullPath, true); } var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService) { Parent = parent, Path = fullPath, FileInfo = fileInfo, CollectionType = collectionType }; // Return null if ignore rules deem that we should do so if (EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(args))) { return null; } // Gather child folder and files if (args.IsDirectory) { var isPhysicalRoot = args.IsPhysicalRoot; // When resolving the root, we need it's grandchildren (children of user views) var flattenFolderDepth = isPhysicalRoot ? 2 : 0; var fileSystemDictionary = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, _fileSystem, _logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: isPhysicalRoot || args.IsVf); // Need to remove subpaths that may have been resolved from shortcuts // Example: if \\server\movies exists, then strip out \\server\movies\action if (isPhysicalRoot) { var paths = NormalizeRootPathList(fileSystemDictionary.Keys); fileSystemDictionary = paths.Select(i => (FileSystemInfo)new DirectoryInfo(i)).ToDictionary(i => i.FullName); } args.FileSystemDictionary = fileSystemDictionary; } // Check to see if we should resolve based on our contents if (args.IsDirectory && !ShouldResolvePathContents(args)) { return null; } return ResolveItem(args); } public IEnumerable NormalizeRootPathList(IEnumerable paths) { var list = paths.Select(_fileSystem.NormalizePath) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.OrdinalIgnoreCase) && list.Any(i => _fileSystem.ContainsSubPath(i, subPath))) .ToList(); foreach (var dupe in dupes) { _logger.Info("Found duplicate path: {0}", dupe); } return list.Except(dupes, StringComparer.OrdinalIgnoreCase); } /// /// Determines whether a path should be ignored based on its contents - called after the contents have been read /// /// The args. /// true if XXXX, false otherwise private static bool ShouldResolvePathContents(ItemResolveArgs args) { // Ignore any folders containing a file called .ignore return !args.ContainsFileSystemEntryByName(".ignore"); } public IEnumerable ResolvePaths(IEnumerable files, IDirectoryService directoryService, Folder parent, string collectionType) { var fileList = files.ToList(); if (parent != null) { foreach (var resolver in MultiItemResolvers) { var result = resolver.ResolveMultiple(parent, fileList, collectionType, directoryService); if (result != null && result.Items.Count > 0) { var items = new List(); items.AddRange(result.Items); foreach (var item in items) { ResolverHelper.SetInitialItemValues(item, parent, _fileSystem, this, directoryService); } items.AddRange(ResolveFileList(result.ExtraFiles, directoryService, parent, collectionType)); return items; } } } return ResolveFileList(fileList, directoryService, parent, collectionType); } private IEnumerable ResolveFileList(IEnumerable fileList, IDirectoryService directoryService, Folder parent, string collectionType) { return fileList.Select(f => { try { return ResolvePath(f, directoryService, parent, collectionType); } catch (Exception ex) { _logger.ErrorException("Error resolving path {0}", ex, f.FullName); return null; } }).Where(i => i != null); } /// /// Creates the root media folder /// /// AggregateFolder. /// Cannot create the root folder until plugins have loaded public AggregateFolder CreateRootFolder() { var rootFolderPath = ConfigurationManager.ApplicationPaths.RootFolderPath; Directory.CreateDirectory(rootFolderPath); var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ?? (AggregateFolder)ResolvePath(new DirectoryInfo(rootFolderPath)); // Add in the plug-in folders foreach (var child in PluginFolderCreators) { var folder = child.GetFolder(); if (folder != null) { if (folder.Id == Guid.Empty) { if (string.IsNullOrWhiteSpace(folder.Path)) { folder.Id = GetNewItemId(folder.GetType().Name, folder.GetType()); } else { folder.Id = GetNewItemId(folder.Path, folder.GetType()); } } var dbItem = GetItemById(folder.Id) as BasePluginFolder; if (dbItem != null && string.Equals(dbItem.Path, folder.Path, StringComparison.OrdinalIgnoreCase)) { folder = dbItem; } rootFolder.AddVirtualChild(folder); RegisterItem(folder); } } return rootFolder; } private UserRootFolder _userRootFolder; private readonly object _syncLock = new object(); public Folder GetUserRootFolder() { if (_userRootFolder == null) { lock (_syncLock) { if (_userRootFolder == null) { var userRootPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; Directory.CreateDirectory(userRootPath); var tmpItem = GetItemById(GetNewItemId(userRootPath, typeof(UserRootFolder))) as UserRootFolder; if (tmpItem == null) { tmpItem = (UserRootFolder)ResolvePath(new DirectoryInfo(userRootPath)); } _userRootFolder = tmpItem; } } } return _userRootFolder; } /// /// Gets a Person /// /// The name. /// Task{Person}. public Person GetPerson(string name) { return GetItemByName(ConfigurationManager.ApplicationPaths.PeoplePath, name); } /// /// Gets a Studio /// /// The name. /// Task{Studio}. public Studio GetStudio(string name) { return GetItemByName(ConfigurationManager.ApplicationPaths.StudioPath, name); } /// /// Gets a Genre /// /// The name. /// Task{Genre}. public Genre GetGenre(string name) { return GetItemByName(ConfigurationManager.ApplicationPaths.GenrePath, name); } /// /// Gets the genre. /// /// The name. /// Task{MusicGenre}. public MusicGenre GetMusicGenre(string name) { return GetItemByName(ConfigurationManager.ApplicationPaths.MusicGenrePath, name); } /// /// Gets the game genre. /// /// The name. /// Task{GameGenre}. public GameGenre GetGameGenre(string name) { return GetItemByName(ConfigurationManager.ApplicationPaths.GameGenrePath, name); } /// /// The us culture /// private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); /// /// Gets a Year /// /// The value. /// Task{Year}. /// public Year GetYear(int value) { if (value <= 0) { throw new ArgumentOutOfRangeException("Years less than or equal to 0 are invalid."); } return GetItemByName(ConfigurationManager.ApplicationPaths.YearPath, value.ToString(UsCulture)); } /// /// Gets the artists path. /// /// The artists path. public string ArtistsPath { get { return Path.Combine(ConfigurationManager.ApplicationPaths.ItemsByNamePath, "artists"); } } /// /// Gets a Genre /// /// The name. /// Task{Genre}. public MusicArtist GetArtist(string name) { return GetItemByName(ArtistsPath, name); } private T GetItemByName(string path, string name) where T : BaseItem, new() { if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentNullException("path"); } if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentNullException("name"); } var validFilename = _fileSystem.GetValidFilename(name).Trim(); string subFolderPrefix = null; var type = typeof(T); if (type == typeof(Person)) { var subFolderIndex = 0; while (!char.IsLetterOrDigit(validFilename[subFolderIndex])) { subFolderIndex++; } subFolderPrefix = validFilename.Substring(subFolderIndex, 1); } var fullPath = string.IsNullOrEmpty(subFolderPrefix) ? Path.Combine(path, validFilename) : Path.Combine(path, subFolderPrefix, validFilename); var id = GetNewItemId(fullPath, type); BaseItem obj; if (!_libraryItemsCache.TryGetValue(id, out obj)) { obj = CreateItemByName(fullPath, name, id); RegisterItem(id, obj); } return obj as T; } private T CreateItemByName(string path, string name, Guid id) where T : BaseItem, new() { var isArtist = typeof(T) == typeof(MusicArtist); if (isArtist) { var existing = RootFolder .GetRecursiveChildren(i => i is T && NameExtensions.AreEqual(i.Name, name)) .Cast() .FirstOrDefault(); if (existing != null) { return existing; } } var fileInfo = new DirectoryInfo(path); var isNew = false; if (!fileInfo.Exists) { try { fileInfo = Directory.CreateDirectory(path); } catch (UnauthorizedAccessException ex) { _logger.Error("Error creating directory {0}", ex, path); throw new Exception(string.Format("Error creating directory {0}", path), ex); } isNew = true; } var item = isNew ? null : GetItemById(id) as T; if (item == null) { item = new T { Name = name, Id = id, DateCreated = _fileSystem.GetCreationTimeUtc(fileInfo), DateModified = _fileSystem.GetLastWriteTimeUtc(fileInfo), Path = path }; } if (isArtist) { (item as MusicArtist).IsAccessedByName = true; } return item; } /// /// Validate and refresh the People sub-set of the IBN. /// The items are stored in the db but not loaded into memory until actually requested by an operation. /// /// The cancellation token. /// The progress. /// Task. public Task ValidatePeople(CancellationToken cancellationToken, IProgress progress) { // Ensure the location is available. Directory.CreateDirectory(ConfigurationManager.ApplicationPaths.PeoplePath); return new PeopleValidator(this, _logger, ConfigurationManager).ValidatePeople(cancellationToken, progress); } /// /// Reloads the root media folder /// /// The progress. /// The cancellation token. /// Task. public Task ValidateMediaLibrary(IProgress progress, CancellationToken cancellationToken) { // Just run the scheduled task so that the user can see it _taskManager.CancelIfRunningAndQueue(); return Task.FromResult(true); } /// /// Queues the library scan. /// public void QueueLibraryScan() { // Just run the scheduled task so that the user can see it _taskManager.QueueScheduledTask(); } /// /// Validates the media library internal. /// /// The progress. /// The cancellation token. /// Task. public async Task ValidateMediaLibraryInternal(IProgress progress, CancellationToken cancellationToken) { _libraryMonitorFactory().Stop(); try { await PerformLibraryValidation(progress, cancellationToken).ConfigureAwait(false); } finally { _libraryMonitorFactory().Start(); } } private async Task PerformLibraryValidation(IProgress progress, CancellationToken cancellationToken) { _logger.Info("Validating media library"); await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); progress.Report(.5); // Start by just validating the children of the root, but go no further await RootFolder.ValidateChildren(new Progress(), cancellationToken, new MetadataRefreshOptions(), recursive: false); progress.Report(1); var userRoot = GetUserRootFolder(); await userRoot.RefreshMetadata(cancellationToken).ConfigureAwait(false); await userRoot.ValidateChildren(new Progress(), cancellationToken, new MetadataRefreshOptions(), recursive: false).ConfigureAwait(false); progress.Report(2); var innerProgress = new ActionableProgress(); innerProgress.RegisterAction(pct => progress.Report(2 + pct * .73)); // Now validate the entire media library await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(), recursive: true).ConfigureAwait(false); progress.Report(75); innerProgress = new ActionableProgress(); innerProgress.RegisterAction(pct => progress.Report(75 + pct * .25)); // Run post-scan tasks await RunPostScanTasks(innerProgress, cancellationToken).ConfigureAwait(false); progress.Report(100); // Bad practice, i know. But we keep a lot in memory, unfortunately. GC.Collect(2, GCCollectionMode.Forced, true); GC.Collect(2, GCCollectionMode.Forced, true); } /// /// Runs the post scan tasks. /// /// The progress. /// The cancellation token. /// Task. private async Task RunPostScanTasks(IProgress progress, CancellationToken cancellationToken) { var tasks = PostscanTasks.ToList(); var numComplete = 0; var numTasks = tasks.Count; foreach (var task in tasks) { var innerProgress = new ActionableProgress(); // Prevent access to modified closure var currentNumComplete = numComplete; innerProgress.RegisterAction(pct => { double innerPercent = (currentNumComplete * 100) + pct; innerPercent /= numTasks; progress.Report(innerPercent); }); try { await task.Run(innerProgress, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { _logger.Info("Post-scan task cancelled: {0}", task.GetType().Name); } catch (Exception ex) { _logger.ErrorException("Error running postscan task", ex); } numComplete++; double percent = numComplete; percent /= numTasks; progress.Report(percent * 100); } progress.Report(100); } /// /// Gets the default view. /// /// IEnumerable{VirtualFolderInfo}. public IEnumerable GetVirtualFolders() { return GetView(ConfigurationManager.ApplicationPaths.DefaultUserViewsPath); } /// /// Gets the view. /// /// The path. /// IEnumerable{VirtualFolderInfo}. private IEnumerable GetView(string path) { return Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly) .Select(dir => new VirtualFolderInfo { Name = Path.GetFileName(dir), Locations = Directory.EnumerateFiles(dir, "*.mblink", SearchOption.TopDirectoryOnly) .Select(_fileSystem.ResolveShortcut) .OrderBy(i => i) .ToList(), CollectionType = GetCollectionType(dir) }); } private string GetCollectionType(string path) { return new DirectoryInfo(path).EnumerateFiles("*.collection", SearchOption.TopDirectoryOnly) .Select(i => _fileSystem.GetFileNameWithoutExtension(i)) .FirstOrDefault(); } /// /// Gets the item by id. /// /// The id. /// BaseItem. /// id public BaseItem GetItemById(Guid id) { if (id == Guid.Empty) { throw new ArgumentNullException("id"); } BaseItem item; if (LibraryItemsCache.TryGetValue(id, out item)) { return item; } item = RetrieveItem(id); if (item != null) { RegisterItem(item); } return item; } public BaseItem GetMemoryItemById(Guid id) { if (id == Guid.Empty) { throw new ArgumentNullException("id"); } BaseItem item; LibraryItemsCache.TryGetValue(id, out item); return item; } /// /// Gets the intros. /// /// The item. /// The user. /// IEnumerable{System.String}. public async Task> GetIntros(BaseItem item, User user) { var tasks = IntroProviders .OrderBy(i => (i.GetType().Name.IndexOf("Default", StringComparison.OrdinalIgnoreCase) == -1 ? 0 : 1)) .Take(1) .Select(i => GetIntros(i, item, user)); var items = await Task.WhenAll(tasks).ConfigureAwait(false); return items .SelectMany(i => i.ToArray()) .Select(ResolveIntro) .Where(i => i != null); } /// /// Gets the intros. /// /// The provider. /// The item. /// The user. /// Task<IEnumerable<IntroInfo>>. private async Task> GetIntros(IIntroProvider provider, BaseItem item, User user) { try { return await provider.GetIntros(item, user).ConfigureAwait(false); } catch (Exception ex) { _logger.ErrorException("Error getting intros", ex); return new List(); } } /// /// Gets all intro files. /// /// IEnumerable{System.String}. public IEnumerable GetAllIntroFiles() { return IntroProviders.SelectMany(i => { try { return i.GetAllIntroFiles().ToList(); } catch (Exception ex) { _logger.ErrorException("Error getting intro files", ex); return new List(); } }); } /// /// Resolves the intro. /// /// The info. /// Video. private Video ResolveIntro(IntroInfo info) { Video video = null; if (info.ItemId.HasValue) { // Get an existing item by Id video = GetItemById(info.ItemId.Value) as Video; if (video == null) { _logger.Error("Unable to locate item with Id {0}.", info.ItemId.Value); } } else if (!string.IsNullOrEmpty(info.Path)) { try { // Try to resolve the path into a video video = ResolvePath(_fileSystem.GetFileSystemInfo(info.Path)) as Video; if (video == null) { _logger.Error("Intro resolver returned null for {0}.", info.Path); } else { // Pull the saved db item that will include metadata var dbItem = GetItemById(video.Id) as Video; if (dbItem != null) { video = dbItem; } } } catch (Exception ex) { _logger.ErrorException("Error resolving path {0}.", ex, info.Path); } } else { _logger.Error("IntroProvider returned an IntroInfo with null Path and ItemId."); } return video; } /// /// Sorts the specified sort by. /// /// The items. /// The user. /// The sort by. /// The sort order. /// IEnumerable{BaseItem}. public IEnumerable Sort(IEnumerable items, User user, IEnumerable sortBy, SortOrder sortOrder) { var isFirst = true; IOrderedEnumerable orderedItems = null; foreach (var orderBy in sortBy.Select(o => GetComparer(o, user)).Where(c => c != null)) { if (isFirst) { orderedItems = sortOrder == SortOrder.Descending ? items.OrderByDescending(i => i, orderBy) : items.OrderBy(i => i, orderBy); } else { orderedItems = sortOrder == SortOrder.Descending ? orderedItems.ThenByDescending(i => i, orderBy) : orderedItems.ThenBy(i => i, orderBy); } isFirst = false; } return orderedItems ?? items; } /// /// Gets the comparer. /// /// The name. /// The user. /// IBaseItemComparer. private IBaseItemComparer GetComparer(string name, User user) { var comparer = Comparers.FirstOrDefault(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase)); if (comparer != null) { // If it requires a user, create a new one, and assign the user if (comparer is IUserBaseItemComparer) { var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType()); userComparer.User = user; userComparer.UserManager = _userManager; userComparer.UserDataRepository = _userDataRepository; return userComparer; } } return comparer; } /// /// Creates the item. /// /// The item. /// The cancellation token. /// Task. public Task CreateItem(BaseItem item, CancellationToken cancellationToken) { return CreateItems(new[] { item }, cancellationToken); } /// /// Creates the items. /// /// The items. /// The cancellation token. /// Task. public async Task CreateItems(IEnumerable items, CancellationToken cancellationToken) { var list = items.ToList(); await ItemRepository.SaveItems(list, cancellationToken).ConfigureAwait(false); foreach (var item in list) { UpdateItemInLibraryCache(item); } if (ItemAdded != null) { foreach (var item in list) { try { ItemAdded(this, new ItemChangeEventArgs { Item = item }); } catch (Exception ex) { _logger.ErrorException("Error in ItemAdded event handler", ex); } } } } /// /// Updates the item. /// /// The item. /// The update reason. /// The cancellation token. /// Task. public async Task UpdateItem(BaseItem item, ItemUpdateType updateReason, CancellationToken cancellationToken) { var locationType = item.LocationType; if (locationType != LocationType.Remote && locationType != LocationType.Virtual) { await _providerManagerFactory().SaveMetadata(item, updateReason).ConfigureAwait(false); } item.DateLastSaved = DateTime.UtcNow; var logName = item.LocationType == LocationType.Remote ? item.Name ?? item.Path : item.Path ?? item.Name; _logger.Debug("Saving {0} to database.", logName); await ItemRepository.SaveItem(item, cancellationToken).ConfigureAwait(false); UpdateItemInLibraryCache(item); if (ItemUpdated != null) { try { ItemUpdated(this, new ItemChangeEventArgs { Item = item, UpdateReason = updateReason }); } catch (Exception ex) { _logger.ErrorException("Error in ItemUpdated event handler", ex); } } } /// /// Reports the item removed. /// /// The item. public void ReportItemRemoved(BaseItem item) { if (ItemRemoved != null) { try { ItemRemoved(this, new ItemChangeEventArgs { Item = item }); } catch (Exception ex) { _logger.ErrorException("Error in ItemRemoved event handler", ex); } } } /// /// Retrieves the item. /// /// The id. /// BaseItem. public BaseItem RetrieveItem(Guid id) { return ItemRepository.RetrieveItem(id); } public IEnumerable GetCollectionFolders(BaseItem item) { while (!(item.Parent is AggregateFolder) && item.Parent != null) { item = item.Parent; } if (item == null) { return new List(); } return GetUserRootFolder().Children .OfType() .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path, StringComparer.OrdinalIgnoreCase)); } public string GetContentType(BaseItem item) { string configuredContentType = GetConfiguredContentType(item, false); if (!string.IsNullOrWhiteSpace(configuredContentType)) { return configuredContentType; } configuredContentType = GetConfiguredContentType(item, true); if (!string.IsNullOrWhiteSpace(configuredContentType)) { return configuredContentType; } return GetInheritedContentType(item); } public string GetInheritedContentType(BaseItem item) { var type = GetTopFolderContentType(item); if (!string.IsNullOrWhiteSpace(type)) { return type; } return item.Parents .Select(GetConfiguredContentType) .LastOrDefault(i => !string.IsNullOrWhiteSpace(i)); } public string GetConfiguredContentType(BaseItem item) { return GetConfiguredContentType(item, false); } public string GetConfiguredContentType(string path) { return GetContentTypeOverride(path, false); } public string GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath) { ICollectionFolder collectionFolder = item as ICollectionFolder; if (collectionFolder != null) { return collectionFolder.CollectionType; } return GetContentTypeOverride(item.ContainingFolderPath, inheritConfiguredPath); } private string GetContentTypeOverride(string path, bool inherit) { var nameValuePair = ConfigurationManager.Configuration.ContentTypes.FirstOrDefault(i => string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase) || (inherit && _fileSystem.ContainsSubPath(i.Name, path))); if (nameValuePair != null) { return nameValuePair.Value; } return null; } private string GetTopFolderContentType(BaseItem item) { while (!(item.Parent is AggregateFolder) && item.Parent != null) { item = item.Parent; } if (item == null) { return null; } return GetUserRootFolder().Children .OfType() .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path)) .Select(i => i.CollectionType) .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i)); } public async Task GetNamedView(User user, string name, string viewType, string sortName, CancellationToken cancellationToken) { if (ConfigurationManager.Configuration.EnableUserSpecificUserViews) { return await GetNamedViewInternal(user, name, null, viewType, sortName, cancellationToken) .ConfigureAwait(false); } var path = Path.Combine(ConfigurationManager.ApplicationPaths.ItemsByNamePath, "views"); path = Path.Combine(path, _fileSystem.GetValidFilename(viewType)); var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView)); var item = GetItemById(id) as UserView; var refresh = false; if (item == null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase)) { Directory.CreateDirectory(path); item = new UserView { Path = path, Id = id, DateCreated = DateTime.UtcNow, Name = name, ViewType = viewType, ForcedSortName = sortName }; await CreateItem(item, cancellationToken).ConfigureAwait(false); refresh = true; } if (!refresh && item != null) { refresh = (DateTime.UtcNow - item.DateLastSaved).TotalHours >= 24; } if (refresh) { await item.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None).ConfigureAwait(false); _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions()); } return item; } public Task GetNamedView(User user, string name, string parentId, string viewType, string sortName, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(parentId)) { throw new ArgumentNullException("parentId"); } return GetNamedViewInternal(user, name, parentId, viewType, sortName, cancellationToken); } private async Task GetNamedViewInternal(User user, string name, string parentId, string viewType, string sortName, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentNullException("name"); } if (string.IsNullOrWhiteSpace(viewType)) { throw new ArgumentNullException("viewType"); } var id = GetNewItemId("37_namedview_" + name + user.Id.ToString("N") + (parentId ?? string.Empty), typeof(UserView)); var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N")); var item = GetItemById(id) as UserView; var isNew = false; if (item == null) { Directory.CreateDirectory(path); item = new UserView { Path = path, Id = id, DateCreated = DateTime.UtcNow, Name = name, ViewType = viewType, ForcedSortName = sortName, UserId = user.Id }; if (!string.IsNullOrWhiteSpace(parentId)) { item.ParentId = new Guid(parentId); } await CreateItem(item, cancellationToken).ConfigureAwait(false); isNew = true; } var refresh = isNew || (DateTime.UtcNow - item.DateLastSaved).TotalHours >= 12; if (refresh) { _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions { // Need to force save to increment DateLastSaved ForceSave = true }); } return item; } public bool IsVideoFile(string path) { var resolver = new VideoResolver(GetNamingOptions(), new PatternsLogger()); return resolver.IsVideoFile(path); } public bool IsAudioFile(string path) { var parser = new AudioFileParser(GetNamingOptions()); return parser.IsAudioFile(path); } public int? GetSeasonNumberFromPath(string path) { return new SeasonPathParser(GetNamingOptions(), new RegexProvider()).Parse(path, true, true).SeasonNumber; } public bool FillMissingEpisodeNumbersFromPath(Episode episode) { var resolver = new EpisodeResolver(GetNamingOptions(), new PatternsLogger()); var fileType = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd || episode.VideoType == VideoType.HdDvd ? FileInfoType.Directory : FileInfoType.File; var locationType = episode.LocationType; var episodeInfo = locationType == LocationType.FileSystem || locationType == LocationType.Offline ? resolver.Resolve(episode.Path, fileType) : new Naming.TV.EpisodeInfo(); if (episodeInfo == null) { episodeInfo = new Naming.TV.EpisodeInfo(); } var changed = false; if (episodeInfo.IsByDate) { if (episode.IndexNumber.HasValue) { episode.IndexNumber = null; changed = true; } if (episode.IndexNumberEnd.HasValue) { episode.IndexNumberEnd = null; changed = true; } if (!episode.PremiereDate.HasValue) { if (episodeInfo.Year.HasValue && episodeInfo.Month.HasValue && episodeInfo.Day.HasValue) { episode.PremiereDate = new DateTime(episodeInfo.Year.Value, episodeInfo.Month.Value, episodeInfo.Day.Value).ToUniversalTime(); } if (episode.PremiereDate.HasValue) { changed = true; } } if (!episode.ProductionYear.HasValue) { episode.ProductionYear = episodeInfo.Year; if (episode.ProductionYear.HasValue) { changed = true; } } if (!episode.ParentIndexNumber.HasValue) { var season = episode.Season; if (season != null) { episode.ParentIndexNumber = season.IndexNumber; } if (episode.ParentIndexNumber.HasValue) { changed = true; } } } else { if (!episode.IndexNumber.HasValue) { episode.IndexNumber = episodeInfo.EpisodeNumber; if (episode.IndexNumber.HasValue) { changed = true; } } if (!episode.IndexNumberEnd.HasValue) { episode.IndexNumberEnd = episodeInfo.EndingEpsiodeNumber; if (episode.IndexNumberEnd.HasValue) { changed = true; } } if (!episode.ParentIndexNumber.HasValue) { episode.ParentIndexNumber = episodeInfo.SeasonNumber; if (!episode.ParentIndexNumber.HasValue) { var season = episode.Season; if (season != null) { episode.ParentIndexNumber = season.IndexNumber; } } if (episode.ParentIndexNumber.HasValue) { changed = true; } } } return changed; } public NamingOptions GetNamingOptions() { var options = new ExtendedNamingOptions(); // These cause apps to have problems options.AudioFileExtensions.Remove(".m3u"); options.AudioFileExtensions.Remove(".wpl"); if (!ConfigurationManager.Configuration.EnableAudioArchiveFiles) { options.AudioFileExtensions.Remove(".rar"); options.AudioFileExtensions.Remove(".zip"); } if (!ConfigurationManager.Configuration.EnableVideoArchiveFiles) { options.VideoFileExtensions.Remove(".rar"); options.VideoFileExtensions.Remove(".zip"); } return options; } public ItemLookupInfo ParseName(string name) { var resolver = new VideoResolver(GetNamingOptions(), new PatternsLogger()); var result = resolver.CleanDateTime(name); var cleanName = resolver.CleanString(result.Name); return new ItemLookupInfo { Name = cleanName.Name, Year = result.Year }; } public IEnumerable