#pragma warning disable CS1591 #pragma warning disable SA1600 using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; using Emby.Naming.Audio; using Emby.Naming.Common; using Emby.Naming.TV; using Emby.Naming.Video; using Emby.Server.Implementations.Library.Resolvers; using Emby.Server.Implementations.Library.Validators; using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.ScheduledTasks; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; 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.LiveTv; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Library; using MediaBrowser.Model.Net; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Tasks; using MediaBrowser.Providers.MediaInfo; using Microsoft.Extensions.Logging; using SortOrder = MediaBrowser.Model.Entities.SortOrder; using VideoResolver = Emby.Naming.Video.VideoResolver; namespace Emby.Server.Implementations.Library { /// /// Class LibraryManager /// public class LibraryManager : ILibraryManager { private NamingOptions _namingOptions; private string[] _videoFileExtensions; /// /// Gets or sets the postscan tasks. /// /// The postscan tasks. private ILibraryPostScanTask[] PostscanTasks { get; set; } /// /// Gets or sets the intro providers. /// /// The intro providers. private IIntroProvider[] IntroProviders { get; set; } /// /// Gets or sets the list of entity resolution ignore rules /// /// The entity resolution ignore rules. private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } /// /// Gets or sets 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 or sets 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; } private readonly Func _libraryMonitorFactory; private readonly Func _providerManagerFactory; private readonly Func _userviewManager; public bool IsScanRunning { get; private set; } private IServerApplicationHost _appHost; private readonly IMediaEncoder _mediaEncoder; /// /// The _library items cache /// private readonly ConcurrentDictionary _libraryItemsCache; /// /// Gets the library items cache. /// /// The library items cache. private ConcurrentDictionary LibraryItemsCache => _libraryItemsCache; private readonly IFileSystem _fileSystem; /// /// Initializes a new instance of the class. /// /// The application host /// The logger factory. /// The task manager. /// The user manager. /// The configuration manager. /// The user data repository. public LibraryManager( IServerApplicationHost appHost, ILoggerFactory loggerFactory, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, IUserDataManager userDataRepository, Func libraryMonitorFactory, IFileSystem fileSystem, Func providerManagerFactory, Func userviewManager, IMediaEncoder mediaEncoder) { _appHost = appHost; _logger = loggerFactory.CreateLogger(nameof(LibraryManager)); _taskManager = taskManager; _userManager = userManager; ConfigurationManager = configurationManager; _userDataRepository = userDataRepository; _libraryMonitorFactory = libraryMonitorFactory; _fileSystem = fileSystem; _providerManagerFactory = providerManagerFactory; _userviewManager = userviewManager; _mediaEncoder = mediaEncoder; _libraryItemsCache = new ConcurrentDictionary(); ConfigurationManager.ConfigurationUpdated += ConfigurationUpdated; RecordConfigurationValues(configurationManager.Configuration); } /// /// Adds the parts. /// /// The rules. /// The resolvers. /// The intro providers. /// The item comparers. /// The post scan tasks. public void AddParts( IEnumerable rules, IEnumerable resolvers, IEnumerable introProviders, IEnumerable itemComparers, IEnumerable postscanTasks) { EntityResolutionIgnoreRules = rules.ToArray(); EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray(); MultiItemResolvers = EntityResolvers.OfType().ToArray(); IntroProviders = introProviders.ToArray(); Comparers = itemComparers.ToArray(); PostscanTasks = postscanTasks.ToArray(); } /// /// The _root folder /// private volatile 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; } } private bool _wizardCompleted; /// /// Records the configuration values. /// /// The configuration. private void RecordConfigurationValues(ServerConfiguration configuration) { _wizardCompleted = configuration.IsStartupWizardCompleted; } /// /// Configurations the updated. /// /// The sender. /// The instance containing the event data. private void ConfigurationUpdated(object sender, EventArgs e) { var config = ConfigurationManager.Configuration; var wizardChanged = config.IsStartupWizardCompleted != _wizardCompleted; RecordConfigurationValues(config); if (wizardChanged) { _taskManager.CancelIfRunningAndQueue(); } } public void RegisterItem(BaseItem item) { if (item == null) { throw new ArgumentNullException(nameof(item)); } if (item is IItemByName) { if (!(item is MusicArtist)) { return; } } else if (!item.IsFolder) { if (!(item is Video) && !(item is LiveTvChannel)) { return; } } LibraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; }); } public void DeleteItem(BaseItem item, DeleteOptions options) { DeleteItem(item, options, false); } public void DeleteItem(BaseItem item, DeleteOptions options, bool notifyParentItem) { if (item == null) { throw new ArgumentNullException(nameof(item)); } var parent = item.GetOwner() ?? item.GetParent(); DeleteItem(item, options, parent, notifyParentItem); } public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem) { if (item == null) { throw new ArgumentNullException(nameof(item)); } if (item.SourceType == SourceType.Channel) { if (options.DeleteFromExternalProvider) { try { var task = BaseItem.ChannelManager.DeleteItem(item); Task.WaitAll(task); } catch (ArgumentException) { // channel no longer installed } } options.DeleteFileLocation = false; } if (item is LiveTvProgram) { _logger.LogDebug( "Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", item.GetType().Name, item.Name ?? "Unknown name", item.Path ?? string.Empty, item.Id); } else { _logger.LogInformation( "Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", item.GetType().Name, item.Name ?? "Unknown name", item.Path ?? string.Empty, item.Id); } var children = item.IsFolder ? ((Folder)item).GetRecursiveChildren(false).ToList() : new List(); foreach (var metadataPath in GetMetadataPaths(item, children)) { if (!Directory.Exists(metadataPath)) { continue; } _logger.LogDebug("Deleting path {MetadataPath}", metadataPath); try { Directory.Delete(metadataPath, true); } catch (Exception ex) { _logger.LogError(ex, "Error deleting {MetadataPath}", metadataPath); } } if (options.DeleteFileLocation && item.IsFileProtocol) { // Assume only the first is required // Add this flag to GetDeletePaths if required in the future var isRequiredForDelete = true; foreach (var fileSystemInfo in item.GetDeletePaths()) { if (Directory.Exists(fileSystemInfo.FullName) || File.Exists(fileSystemInfo.FullName)) { try { _logger.LogDebug("Deleting path {path}", fileSystemInfo.FullName); if (fileSystemInfo.IsDirectory) { Directory.Delete(fileSystemInfo.FullName, true); } else { File.Delete(fileSystemInfo.FullName); } } catch (IOException) { if (isRequiredForDelete) { throw; } } catch (UnauthorizedAccessException) { if (isRequiredForDelete) { throw; } } } isRequiredForDelete = false; } } item.SetParent(null); ItemRepository.DeleteItem(item.Id, CancellationToken.None); foreach (var child in children) { ItemRepository.DeleteItem(child.Id, CancellationToken.None); } _libraryItemsCache.TryRemove(item.Id, out BaseItem removed); ReportItemRemoved(item, parent); } private static 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. /// The resolvers. /// BaseItem. private BaseItem ResolveItem(ItemResolveArgs args, IItemResolver[] resolvers) { var item = (resolvers ?? 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.LogError(ex, "Error in {resolver} resolving {path}", resolver.GetType().Name, args.Path); return null; } } public Guid GetNewItemId(string key, Type type) { return GetNewItemIdInternal(key, type, false); } private Guid GetNewItemIdInternal(string key, Type type, bool forceCaseInsensitive) { if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException(nameof(key)); } if (type == null) { throw new ArgumentNullException(nameof(type)); } if (key.StartsWith(ConfigurationManager.ApplicationPaths.ProgramDataPath, StringComparison.Ordinal)) { // 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("/", "\\"); } if (forceCaseInsensitive || !ConfigurationManager.Configuration.EnableCaseSensitiveItemIds) { key = key.ToLowerInvariant(); } key = type.FullName + key; return key.GetMD5(); } public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null) => ResolvePath(fileInfo, new DirectoryService(_fileSystem), null, parent); private BaseItem ResolvePath( FileSystemMetadata fileInfo, IDirectoryService directoryService, IItemResolver[] resolvers, Folder parent = null, string collectionType = null, LibraryOptions libraryOptions = null) { if (fileInfo == null) { throw new ArgumentNullException(nameof(fileInfo)); } var fullPath = fileInfo.FullName; if (string.IsNullOrEmpty(collectionType) && parent != null) { collectionType = GetContentTypeOverride(fullPath, true); } var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService) { Parent = parent, Path = fullPath, FileInfo = fileInfo, CollectionType = collectionType, LibraryOptions = libraryOptions }; // Return null if ignore rules deem that we should do so if (IgnoreFile(args.FileInfo, args.Parent)) { 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; FileSystemMetadata[] files; var isVf = args.IsVf; try { files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, _fileSystem, _appHost, _logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: isPhysicalRoot || isVf); } catch (Exception ex) { if (parent != null && parent.IsPhysicalRoot) { _logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPhysicalRoot, isVf); files = Array.Empty(); } else { throw; } } // Need to remove subpaths that may have been resolved from shortcuts // Example: if \\server\movies exists, then strip out \\server\movies\action if (isPhysicalRoot) { files = NormalizeRootPathList(files).ToArray(); } args.FileSystemChildren = files; } // Check to see if we should resolve based on our contents if (args.IsDirectory && !ShouldResolvePathContents(args)) { return null; } return ResolveItem(args, resolvers); } public bool IgnoreFile(FileSystemMetadata file, BaseItem parent) => EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(file, parent)); public List NormalizeRootPathList(IEnumerable paths) { var originalList = paths.ToList(); var list = originalList.Where(i => i.IsDirectory) .Select(i => _fileSystem.NormalizePath(i.FullName)) .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.LogInformation("Found duplicate path: {0}", dupe); } var newList = list.Except(dupes, StringComparer.OrdinalIgnoreCase).Select(_fileSystem.GetDirectoryInfo).ToList(); newList.AddRange(originalList.Where(i => !i.IsDirectory)); return newList; } /// /// 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, LibraryOptions libraryOptions, string collectionType) { return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers); } public IEnumerable ResolvePaths( IEnumerable files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, string collectionType, IItemResolver[] resolvers) { var fileList = files.Where(i => !IgnoreFile(i, parent)).ToList(); if (parent != null) { var multiItemResolvers = resolvers == null ? MultiItemResolvers : resolvers.OfType().ToArray(); 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, resolvers, libraryOptions)); return items; } } } return ResolveFileList(fileList, directoryService, parent, collectionType, resolvers, libraryOptions); } private IEnumerable ResolveFileList( IEnumerable fileList, IDirectoryService directoryService, Folder parent, string collectionType, IItemResolver[] resolvers, LibraryOptions libraryOptions) { return fileList.Select(f => { try { return ResolvePath(f, directoryService, resolvers, parent, collectionType, libraryOptions); } catch (Exception ex) { _logger.LogError(ex, "Error resolving path {path}", 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 ?? ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath))).DeepCopy(); // In case program data folder was moved if (!string.Equals(rootFolder.Path, rootFolderPath, StringComparison.Ordinal)) { _logger.LogInformation("Resetting root folder path to {0}", rootFolderPath); rootFolder.Path = rootFolderPath; } // Add in the plug-in folders var path = Path.Combine(ConfigurationManager.ApplicationPaths.DataPath, "playlists"); Directory.CreateDirectory(path); Folder folder = new PlaylistsFolder { Path = path }; if (folder.Id.Equals(Guid.Empty)) { if (string.IsNullOrEmpty(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; } if (folder.ParentId != rootFolder.Id) { folder.ParentId = rootFolder.Id; folder.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None); } rootFolder.AddVirtualChild(folder); RegisterItem(folder); return rootFolder; } private volatile UserRootFolder _userRootFolder; private readonly object _syncLock = new object(); public Folder GetUserRootFolder() { if (_userRootFolder == null) { lock (_syncLock) { if (_userRootFolder == null) { var userRootPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; _logger.LogDebug("Creating userRootPath at {path}", userRootPath); Directory.CreateDirectory(userRootPath); var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder)); UserRootFolder tmpItem = null; try { tmpItem = GetItemById(newItemId) as UserRootFolder; } catch (Exception ex) { _logger.LogError(ex, "Error creating UserRootFolder {path}", newItemId); } if (tmpItem == null) { _logger.LogDebug("Creating new userRootFolder with DeepCopy"); tmpItem = ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath))).DeepCopy(); } // In case program data folder was moved if (!string.Equals(tmpItem.Path, userRootPath, StringComparison.Ordinal)) { _logger.LogInformation("Resetting user root folder path to {0}", userRootPath); tmpItem.Path = userRootPath; } _userRootFolder = tmpItem; _logger.LogDebug("Setting userRootFolder: {folder}", _userRootFolder); } } } return _userRootFolder; } public BaseItem FindByPath(string path, bool? isFolder) { // If this returns multiple items it could be tricky figuring out which one is correct. // In most cases, the newest one will be and the others obsolete but not yet cleaned up if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException(nameof(path)); } var query = new InternalItemsQuery { Path = path, IsFolder = isFolder, OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) }, Limit = 1, DtoOptions = new DtoOptions(true) }; return GetItemList(query) .FirstOrDefault(); } /// /// Gets the person. /// /// The name. /// Task{Person}. public Person GetPerson(string name) { return CreateItemByName(Person.GetPath, name, new DtoOptions(true)); } /// /// Gets the studio. /// /// The name. /// Task{Studio}. public Studio GetStudio(string name) { return CreateItemByName(Studio.GetPath, name, new DtoOptions(true)); } public Guid GetStudioId(string name) { return GetItemByNameId(Studio.GetPath, name); } public Guid GetGenreId(string name) { return GetItemByNameId(Genre.GetPath, name); } public Guid GetMusicGenreId(string name) { return GetItemByNameId(MusicGenre.GetPath, name); } /// /// Gets the genre. /// /// The name. /// Task{Genre}. public Genre GetGenre(string name) { return CreateItemByName(Genre.GetPath, name, new DtoOptions(true)); } /// /// Gets the music genre. /// /// The name. /// Task{MusicGenre}. public MusicGenre GetMusicGenre(string name) { return CreateItemByName(MusicGenre.GetPath, name, new DtoOptions(true)); } /// /// Gets the year. /// /// The value. /// Task{Year}. public Year GetYear(int value) { if (value <= 0) { throw new ArgumentOutOfRangeException(nameof(value), "Years less than or equal to 0 are invalid."); } var name = value.ToString(CultureInfo.InvariantCulture); return CreateItemByName(Year.GetPath, name, new DtoOptions(true)); } /// /// Gets a Genre /// /// The name. /// Task{Genre}. public MusicArtist GetArtist(string name) { return GetArtist(name, new DtoOptions(true)); } public MusicArtist GetArtist(string name, DtoOptions options) { return CreateItemByName(MusicArtist.GetPath, name, options); } private T CreateItemByName(Func getPathFn, string name, DtoOptions options) where T : BaseItem, new() { if (typeof(T) == typeof(MusicArtist)) { var existing = GetItemList(new InternalItemsQuery { IncludeItemTypes = new[] { typeof(T).Name }, Name = name, DtoOptions = options }).Cast() .OrderBy(i => i.IsAccessedByName ? 1 : 0) .Cast() .FirstOrDefault(); if (existing != null) { return existing; } } var id = GetItemByNameId(getPathFn, name); var item = GetItemById(id) as T; if (item == null) { var path = getPathFn(name); item = new T { Name = name, Id = id, DateCreated = DateTime.UtcNow, DateModified = DateTime.UtcNow, Path = path }; CreateItem(item, null); } return item; } private Guid GetItemByNameId(Func getPathFn, string name) where T : BaseItem, new() { var path = getPathFn(name); var forceCaseInsensitiveId = ConfigurationManager.Configuration.EnableNormalizedItemByNameIds; return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId); } /// /// 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, _fileSystem).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.CompletedTask; } /// /// 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) { IsScanRunning = true; _libraryMonitorFactory().Stop(); try { await PerformLibraryValidation(progress, cancellationToken).ConfigureAwait(false); } finally { _libraryMonitorFactory().Start(); IsScanRunning = false; } } private async Task ValidateTopLibraryFolders(CancellationToken cancellationToken) { await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); // Start by just validating the children of the root, but go no further await RootFolder.ValidateChildren( new SimpleProgress(), cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: false).ConfigureAwait(false); await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false); await GetUserRootFolder().ValidateChildren( new SimpleProgress(), cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: false).ConfigureAwait(false); // Quickly scan CollectionFolders for changes foreach (var folder in GetUserRootFolder().Children.OfType()) { await folder.RefreshMetadata(cancellationToken).ConfigureAwait(false); } } private async Task PerformLibraryValidation(IProgress progress, CancellationToken cancellationToken) { _logger.LogInformation("Validating media library"); await ValidateTopLibraryFolders(cancellationToken).ConfigureAwait(false); var innerProgress = new ActionableProgress(); innerProgress.RegisterAction(pct => progress.Report(pct * pct * 0.96)); // Validate the entire media library await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true).ConfigureAwait(false); progress.Report(96); innerProgress = new ActionableProgress(); innerProgress.RegisterAction(pct => progress.Report(96 + (pct * .04))); await RunPostScanTasks(innerProgress, cancellationToken).ConfigureAwait(false); progress.Report(100); } /// /// 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 = pct; innerPercent /= 100; innerPercent += currentNumComplete; innerPercent /= numTasks; innerPercent *= 100; progress.Report(innerPercent); }); _logger.LogDebug("Running post-scan task {0}", task.GetType().Name); try { await task.Run(innerProgress, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { _logger.LogInformation("Post-scan task cancelled: {0}", task.GetType().Name); throw; } catch (Exception ex) { _logger.LogError(ex, "Error running post-scan task"); } numComplete++; double percent = numComplete; percent /= numTasks; progress.Report(percent * 100); } ItemRepository.UpdateInheritedValues(cancellationToken); progress.Report(100); } /// /// Gets the default view. /// /// IEnumerable{VirtualFolderInfo}. public List GetVirtualFolders() { return GetVirtualFolders(false); } public List GetVirtualFolders(bool includeRefreshState) { _logger.LogDebug("Getting topLibraryFolders"); var topLibraryFolders = GetUserRootFolder().Children.ToList(); _logger.LogDebug("Getting refreshQueue"); var refreshQueue = includeRefreshState ? _providerManagerFactory().GetRefreshQueue() : null; return _fileSystem.GetDirectoryPaths(ConfigurationManager.ApplicationPaths.DefaultUserViewsPath) .Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders, refreshQueue)) .OrderBy(i => i.Name) .ToList(); } private VirtualFolderInfo GetVirtualFolderInfo(string dir, List allCollectionFolders, Dictionary refreshQueue) { var info = new VirtualFolderInfo { Name = Path.GetFileName(dir), Locations = _fileSystem.GetFilePaths(dir, false) .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) .Select(i => { try { return _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(i)); } catch (Exception ex) { _logger.LogError(ex, "Error resolving shortcut file {file}", i); return null; } }) .Where(i => i != null) .OrderBy(i => i) .ToArray(), CollectionType = GetCollectionType(dir) }; var libraryFolder = allCollectionFolders.FirstOrDefault(i => string.Equals(i.Path, dir, StringComparison.OrdinalIgnoreCase)); if (libraryFolder != null && libraryFolder.HasImage(ImageType.Primary)) { info.PrimaryImageItemId = libraryFolder.Id.ToString("N", CultureInfo.InvariantCulture); } if (libraryFolder != null) { info.ItemId = libraryFolder.Id.ToString("N", CultureInfo.InvariantCulture); info.LibraryOptions = GetLibraryOptions(libraryFolder); if (refreshQueue != null) { info.RefreshProgress = libraryFolder.GetRefreshProgress(); info.RefreshStatus = info.RefreshProgress.HasValue ? "Active" : refreshQueue.ContainsKey(libraryFolder.Id) ? "Queued" : "Idle"; } } return info; } private string GetCollectionType(string path) { return _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false) .Select(Path.GetFileNameWithoutExtension) .FirstOrDefault(i => !string.IsNullOrEmpty(i)); } /// /// Gets the item by id. /// /// The id. /// BaseItem. /// id public BaseItem GetItemById(Guid id) { if (id == Guid.Empty) { throw new ArgumentException("Guid can't be empty", nameof(id)); } if (LibraryItemsCache.TryGetValue(id, out BaseItem item)) { return item; } item = RetrieveItem(id); if (item != null) { RegisterItem(item); } return item; } public List GetItemList(InternalItemsQuery query, bool allowExternalContent) { if (query.Recursive && query.ParentId != Guid.Empty) { var parent = GetItemById(query.ParentId); if (parent != null) { SetTopParentIdsOrAncestors(query, new List { parent }); } } if (query.User != null) { AddUserToQuery(query, query.User, allowExternalContent); } return ItemRepository.GetItemList(query); } public List GetItemList(InternalItemsQuery query) { return GetItemList(query, true); } public int GetCount(InternalItemsQuery query) { if (query.Recursive && !query.ParentId.Equals(Guid.Empty)) { var parent = GetItemById(query.ParentId); if (parent != null) { SetTopParentIdsOrAncestors(query, new List { parent }); } } if (query.User != null) { AddUserToQuery(query, query.User); } return ItemRepository.GetCount(query); } public List GetItemList(InternalItemsQuery query, List parents) { SetTopParentIdsOrAncestors(query, parents); if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0) { if (query.User != null) { AddUserToQuery(query, query.User); } } return ItemRepository.GetItemList(query); } public QueryResult QueryItems(InternalItemsQuery query) { if (query.User != null) { AddUserToQuery(query, query.User); } if (query.EnableTotalRecordCount) { return ItemRepository.GetItems(query); } return new QueryResult { Items = ItemRepository.GetItemList(query).ToArray() }; } public List GetItemIds(InternalItemsQuery query) { if (query.User != null) { AddUserToQuery(query, query.User); } return ItemRepository.GetItemIdsList(query); } public QueryResult<(BaseItem, ItemCounts)> GetStudios(InternalItemsQuery query) { if (query.User != null) { AddUserToQuery(query, query.User); } SetTopParentOrAncestorIds(query); return ItemRepository.GetStudios(query); } public QueryResult<(BaseItem, ItemCounts)> GetGenres(InternalItemsQuery query) { if (query.User != null) { AddUserToQuery(query, query.User); } SetTopParentOrAncestorIds(query); return ItemRepository.GetGenres(query); } public QueryResult<(BaseItem, ItemCounts)> GetMusicGenres(InternalItemsQuery query) { if (query.User != null) { AddUserToQuery(query, query.User); } SetTopParentOrAncestorIds(query); return ItemRepository.GetMusicGenres(query); } public QueryResult<(BaseItem, ItemCounts)> GetAllArtists(InternalItemsQuery query) { if (query.User != null) { AddUserToQuery(query, query.User); } SetTopParentOrAncestorIds(query); return ItemRepository.GetAllArtists(query); } public QueryResult<(BaseItem, ItemCounts)> GetArtists(InternalItemsQuery query) { if (query.User != null) { AddUserToQuery(query, query.User); } SetTopParentOrAncestorIds(query); return ItemRepository.GetArtists(query); } private void SetTopParentOrAncestorIds(InternalItemsQuery query) { if (query.AncestorIds.Length == 0) { return; } var parents = query.AncestorIds.Select(i => GetItemById(i)).ToList(); if (parents.All(i => i is ICollectionFolder || i is UserView)) { // Optimize by querying against top level views query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray(); query.AncestorIds = Array.Empty(); // Prevent searching in all libraries due to empty filter if (query.TopParentIds.Length == 0) { query.TopParentIds = new[] { Guid.NewGuid() }; } } } public QueryResult<(BaseItem, ItemCounts)> GetAlbumArtists(InternalItemsQuery query) { if (query.User != null) { AddUserToQuery(query, query.User); } SetTopParentOrAncestorIds(query); return ItemRepository.GetAlbumArtists(query); } public QueryResult GetItemsResult(InternalItemsQuery query) { if (query.Recursive && !query.ParentId.Equals(Guid.Empty)) { var parent = GetItemById(query.ParentId); if (parent != null) { SetTopParentIdsOrAncestors(query, new List { parent }); } } if (query.User != null) { AddUserToQuery(query, query.User); } if (query.EnableTotalRecordCount) { return ItemRepository.GetItems(query); } var list = ItemRepository.GetItemList(query); return new QueryResult { Items = list }; } private void SetTopParentIdsOrAncestors(InternalItemsQuery query, List parents) { if (parents.All(i => i is ICollectionFolder || i is UserView)) { // Optimize by querying against top level views query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray(); // Prevent searching in all libraries due to empty filter if (query.TopParentIds.Length == 0) { query.TopParentIds = new[] { Guid.NewGuid() }; } } else { // We need to be able to query from any arbitrary ancestor up the tree query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).ToArray(); // Prevent searching in all libraries due to empty filter if (query.AncestorIds.Length == 0) { query.AncestorIds = new[] { Guid.NewGuid() }; } } query.Parent = null; } private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true) { if (query.AncestorIds.Length == 0 && query.ParentId.Equals(Guid.Empty) && query.ChannelIds.Length == 0 && query.TopParentIds.Length == 0 && string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) && string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) && query.ItemIds.Length == 0) { var userViews = _userviewManager().GetUserViews(new UserViewQuery { UserId = user.Id, IncludeHidden = true, IncludeExternalContent = allowExternalContent }); query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).ToArray(); } } private IEnumerable GetTopParentIdsForQuery(BaseItem item, User user) { if (item is UserView view) { if (string.Equals(view.ViewType, CollectionType.LiveTv, StringComparison.Ordinal)) { return new[] { view.Id }; } // Translate view into folders if (!view.DisplayParentId.Equals(Guid.Empty)) { var displayParent = GetItemById(view.DisplayParentId); if (displayParent != null) { return GetTopParentIdsForQuery(displayParent, user); } return Array.Empty(); } if (!view.ParentId.Equals(Guid.Empty)) { var displayParent = GetItemById(view.ParentId); if (displayParent != null) { return GetTopParentIdsForQuery(displayParent, user); } return Array.Empty(); } // Handle grouping if (user != null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType) && user.Configuration.GroupedFolders.Length > 0) { return GetUserRootFolder() .GetChildren(user, true) .OfType() .Where(i => string.IsNullOrEmpty(i.CollectionType) || string.Equals(i.CollectionType, view.ViewType, StringComparison.OrdinalIgnoreCase)) .Where(i => user.IsFolderGrouped(i.Id)) .SelectMany(i => GetTopParentIdsForQuery(i, user)); } return Array.Empty(); } if (item is CollectionFolder collectionFolder) { return collectionFolder.PhysicalFolderIds; } var topParent = item.GetTopParent(); if (topParent != null) { return new[] { topParent.Id }; } return Array.Empty(); } /// /// 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.LogError(ex, "Error getting intros"); 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.LogError(ex, "Error getting intro files"); 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.LogError("Unable to locate item with Id {ID}.", 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.LogError("Intro resolver returned null for {path}.", info.Path); } else { // Pull the saved db item that will include metadata var dbItem = GetItemById(video.Id) as Video; if (dbItem != null) { video = dbItem; } else { return null; } } } catch (Exception ex) { _logger.LogError(ex, "Error resolving path {path}.", info.Path); } } else { _logger.LogError("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; } public IEnumerable Sort(IEnumerable items, User user, IEnumerable> orderByList) { var isFirst = true; IOrderedEnumerable orderedItems = null; foreach (var orderBy in orderByList) { var comparer = GetComparer(orderBy.Item1, user); if (comparer == null) { continue; } var sortOrder = orderBy.Item2; if (isFirst) { orderedItems = sortOrder == SortOrder.Descending ? items.OrderByDescending(i => i, comparer) : items.OrderBy(i => i, comparer); } else { orderedItems = sortOrder == SortOrder.Descending ? orderedItems.ThenByDescending(i => i, comparer) : orderedItems.ThenBy(i => i, comparer); } 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 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 parent item. public void CreateItem(BaseItem item, BaseItem parent) { CreateItems(new[] { item }, parent, CancellationToken.None); } /// /// Creates the items. /// /// The items. /// The parent item /// The cancellation token. public void CreateItems(IEnumerable items, BaseItem parent, CancellationToken cancellationToken) { // Don't iterate multiple times var itemsList = items.ToList(); ItemRepository.SaveItems(itemsList, cancellationToken); foreach (var item in itemsList) { RegisterItem(item); } if (ItemAdded != null) { foreach (var item in itemsList) { // With the live tv guide this just creates too much noise if (item.SourceType != SourceType.Library) { continue; } try { ItemAdded( this, new ItemChangeEventArgs { Item = item, Parent = parent ?? item.GetParent() }); } catch (Exception ex) { _logger.LogError(ex, "Error in ItemAdded event handler"); } } } } public void UpdateImages(BaseItem item) { ItemRepository.SaveImages(item); RegisterItem(item); } /// /// Updates the item. /// public void UpdateItems(IEnumerable items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) { // Don't iterate multiple times var itemsList = items.ToList(); foreach (var item in itemsList) { if (item.IsFileProtocol) { _providerManagerFactory().SaveMetadata(item, updateReason); } item.DateLastSaved = DateTime.UtcNow; RegisterItem(item); } ItemRepository.SaveItems(itemsList, cancellationToken); if (ItemUpdated != null) { foreach (var item in itemsList) { // With the live tv guide this just creates too much noise if (item.SourceType != SourceType.Library) { continue; } try { ItemUpdated( this, new ItemChangeEventArgs { Item = item, Parent = parent, UpdateReason = updateReason }); } catch (Exception ex) { _logger.LogError(ex, "Error in ItemUpdated event handler"); } } } } /// /// Updates the item. /// /// The item. /// The parent item. /// The update reason. /// The cancellation token. public void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) { UpdateItems(new[] { item }, parent, updateReason, cancellationToken); } /// /// Reports the item removed. /// /// The item. /// The parent item. public void ReportItemRemoved(BaseItem item, BaseItem parent) { if (ItemRemoved != null) { try { ItemRemoved( this, new ItemChangeEventArgs { Item = item, Parent = parent }); } catch (Exception ex) { _logger.LogError(ex, "Error in ItemRemoved event handler"); } } } /// /// Retrieves the item. /// /// The id. /// BaseItem. public BaseItem RetrieveItem(Guid id) { return ItemRepository.RetrieveItem(id); } public List GetCollectionFolders(BaseItem item) { while (item != null) { var parent = item.GetParent(); if (parent == null || parent is AggregateFolder) { break; } item = parent; } if (item == null) { return new List(); } return GetCollectionFoldersInternal(item, GetUserRootFolder().Children.OfType().ToList()); } public List GetCollectionFolders(BaseItem item, List allUserRootChildren) { while (item != null) { var parent = item.GetParent(); if (parent == null || parent is AggregateFolder) { break; } item = parent; } if (item == null) { return new List(); } return GetCollectionFoldersInternal(item, allUserRootChildren); } private static List GetCollectionFoldersInternal(BaseItem item, List allUserRootChildren) { return allUserRootChildren .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path, StringComparer.OrdinalIgnoreCase)) .ToList(); } public LibraryOptions GetLibraryOptions(BaseItem item) { if (!(item is CollectionFolder collectionFolder)) { collectionFolder = GetCollectionFolders(item) .OfType() .FirstOrDefault(); } return collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions(); } public string GetContentType(BaseItem item) { string configuredContentType = GetConfiguredContentType(item, false); if (!string.IsNullOrEmpty(configuredContentType)) { return configuredContentType; } configuredContentType = GetConfiguredContentType(item, true); if (!string.IsNullOrEmpty(configuredContentType)) { return configuredContentType; } return GetInheritedContentType(item); } public string GetInheritedContentType(BaseItem item) { var type = GetTopFolderContentType(item); if (!string.IsNullOrEmpty(type)) { return type; } return item.GetParents() .Select(GetConfiguredContentType) .LastOrDefault(i => !string.IsNullOrEmpty(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) { if (item is ICollectionFolder collectionFolder) { return collectionFolder.CollectionType; } return GetContentTypeOverride(item.ContainingFolderPath, inheritConfiguredPath); } private string GetContentTypeOverride(string path, bool inherit) { var nameValuePair = ConfigurationManager.Configuration.ContentTypes .FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path) || (inherit && !string.IsNullOrEmpty(i.Name) && _fileSystem.ContainsSubPath(i.Name, path))); return nameValuePair?.Value; } private string GetTopFolderContentType(BaseItem item) { if (item == null) { return null; } while (!item.ParentId.Equals(Guid.Empty)) { var parent = item.GetParent(); if (parent == null || parent is AggregateFolder) { break; } item = parent; } 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.IsNullOrEmpty(i)); } private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24); public UserView GetNamedView( User user, string name, string viewType, string sortName) { return GetNamedView(user, name, Guid.Empty, viewType, sortName); } public UserView GetNamedView( string name, string viewType, string sortName) { var path = Path.Combine( ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views", _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 }; CreateItem(item, null); refresh = true; } if (refresh) { item.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None); _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal); } return item; } public UserView GetNamedView( User user, string name, Guid parentId, string viewType, string sortName) { var parentIdString = parentId.Equals(Guid.Empty) ? null : parentId.ToString("N", CultureInfo.InvariantCulture); var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType ?? string.Empty); var id = GetNewItemId(idValues, typeof(UserView)); var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N", CultureInfo.InvariantCulture)); 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, DisplayParentId = parentId }; CreateItem(item, null); isNew = true; } var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; if (!refresh && !item.DisplayParentId.Equals(Guid.Empty)) { var displayParent = GetItemById(item.DisplayParentId); refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; } if (refresh) { _providerManagerFactory().QueueRefresh( item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { // Need to force save to increment DateLastSaved ForceSave = true }, RefreshPriority.Normal); } return item; } public UserView GetShadowView( BaseItem parent, string viewType, string sortName) { if (parent == null) { throw new ArgumentNullException(nameof(parent)); } var name = parent.Name; var parentId = parent.Id; var idValues = "38_namedview_" + name + parentId + (viewType ?? string.Empty); var id = GetNewItemId(idValues, typeof(UserView)); var path = parent.Path; 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 }; item.DisplayParentId = parentId; CreateItem(item, null); isNew = true; } var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; if (!refresh && !item.DisplayParentId.Equals(Guid.Empty)) { var displayParent = GetItemById(item.DisplayParentId); refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; } if (refresh) { _providerManagerFactory().QueueRefresh( item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { // Need to force save to increment DateLastSaved ForceSave = true }, RefreshPriority.Normal); } return item; } public UserView GetNamedView( string name, Guid parentId, string viewType, string sortName, string uniqueId) { if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException(nameof(name)); } var parentIdString = parentId.Equals(Guid.Empty) ? null : parentId.ToString("N", CultureInfo.InvariantCulture); var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType ?? string.Empty); if (!string.IsNullOrEmpty(uniqueId)) { idValues += uniqueId; } var id = GetNewItemId(idValues, typeof(UserView)); var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N", CultureInfo.InvariantCulture)); 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 }; item.DisplayParentId = parentId; CreateItem(item, null); isNew = true; } if (!string.Equals(viewType, item.ViewType, StringComparison.OrdinalIgnoreCase)) { item.ViewType = viewType; item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); } var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; if (!refresh && !item.DisplayParentId.Equals(Guid.Empty)) { var displayParent = GetItemById(item.DisplayParentId); refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; } if (refresh) { _providerManagerFactory().QueueRefresh( item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { // Need to force save to increment DateLastSaved ForceSave = true }, RefreshPriority.Normal); } return item; } public void AddExternalSubtitleStreams( List streams, string videoPath, string[] files) { new SubtitleResolver(BaseItem.LocalizationManager, _fileSystem).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files); } public bool IsVideoFile(string path, LibraryOptions libraryOptions) { var resolver = new VideoResolver(GetNamingOptions()); return resolver.IsVideoFile(path); } public bool IsVideoFile(string path) { return IsVideoFile(path, new LibraryOptions()); } public bool IsAudioFile(string path, LibraryOptions libraryOptions) { var parser = new AudioFileParser(GetNamingOptions()); return parser.IsAudioFile(path); } public bool IsAudioFile(string path) { return IsAudioFile(path, new LibraryOptions()); } public int? GetSeasonNumberFromPath(string path) { return SeasonPathParser.Parse(path, true, true).SeasonNumber; } public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh) { var series = episode.Series; bool? isAbsoluteNaming = series == null ? false : string.Equals(series.DisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase); if (!isAbsoluteNaming.Value) { // In other words, no filter applied isAbsoluteNaming = null; } var resolver = new EpisodeResolver(GetNamingOptions()); var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd; var episodeInfo = episode.IsFileProtocol ? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) : new Naming.TV.EpisodeInfo(); if (episodeInfo == null) { episodeInfo = new Naming.TV.EpisodeInfo(); } try { var libraryOptions = GetLibraryOptions(episode); if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase)) { // Read from metadata var mediaInfo = _mediaEncoder.GetMediaInfo(new MediaInfoRequest { MediaSource = episode.GetMediaSources(false)[0], MediaType = DlnaProfileType.Video }, CancellationToken.None).GetAwaiter().GetResult(); if (mediaInfo.ParentIndexNumber > 0) { episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber; } if (mediaInfo.IndexNumber > 0) { episodeInfo.EpisodeNumber = mediaInfo.IndexNumber; } if (!string.IsNullOrEmpty(mediaInfo.ShowName)) { episodeInfo.SeriesName = mediaInfo.ShowName; } } } catch (Exception ex) { _logger.LogError(ex, "Error reading the episode informations with ffprobe. Episode: {EpisodeInfo}", episodeInfo.Path); } 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; } } } else { if (!episode.IndexNumber.HasValue || forceRefresh) { if (episode.IndexNumber != episodeInfo.EpisodeNumber) { changed = true; } episode.IndexNumber = episodeInfo.EpisodeNumber; } if (!episode.IndexNumberEnd.HasValue || forceRefresh) { if (episode.IndexNumberEnd != episodeInfo.EndingEpsiodeNumber) { changed = true; } episode.IndexNumberEnd = episodeInfo.EndingEpsiodeNumber; } if (!episode.ParentIndexNumber.HasValue || forceRefresh) { if (episode.ParentIndexNumber != episodeInfo.SeasonNumber) { changed = true; } episode.ParentIndexNumber = episodeInfo.SeasonNumber; } } if (!episode.ParentIndexNumber.HasValue) { var season = episode.Season; if (season != null) { episode.ParentIndexNumber = season.IndexNumber; } else { /* Anime series don't generally have a season in their file name, however, tvdb needs a season to correctly get the metadata. Hence, a null season needs to be filled with something. */ //FIXME perhaps this would be better for tvdb parser to ask for season 1 if no season is specified episode.ParentIndexNumber = 1; } if (episode.ParentIndexNumber.HasValue) { changed = true; } } return changed; } public NamingOptions GetNamingOptions() { if (_namingOptions == null) { _namingOptions = new NamingOptions(); _videoFileExtensions = _namingOptions.VideoFileExtensions; } return _namingOptions; } public ItemLookupInfo ParseName(string name) { var resolver = new VideoResolver(GetNamingOptions()); var result = resolver.CleanDateTime(name); return new ItemLookupInfo { Name = resolver.TryCleanString(result.Name, out var newName) ? newName.ToString() : result.Name, Year = result.Year }; } public IEnumerable