From 3eb4091808735858b01855d298226d239be464af Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Thu, 3 Nov 2016 02:37:52 -0400 Subject: move additional classes to new server lib --- .../Library/LibraryManager.cs | 3066 ++++++++++++++++++++ 1 file changed, 3066 insertions(+) create mode 100644 Emby.Server.Implementations/Library/LibraryManager.cs (limited to 'Emby.Server.Implementations/Library/LibraryManager.cs') diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs new file mode 100644 index 000000000..7ae00d94c --- /dev/null +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -0,0 +1,3066 @@ +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Progress; +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.Model.Querying; +using MediaBrowser.Naming.Audio; +using MediaBrowser.Naming.Common; +using MediaBrowser.Naming.TV; +using MediaBrowser.Naming.Video; +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.Server.Implementations.Library.Resolvers; +using Emby.Server.Implementations.Library.Validators; +using Emby.Server.Implementations.Logging; +using Emby.Server.Implementations.ScheduledTasks; +using MediaBrowser.Model.IO; +using MediaBrowser.Controller.Channels; +using MediaBrowser.Model.Channels; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Library; +using MediaBrowser.Model.Net; +using SortOrder = MediaBrowser.Model.Entities.SortOrder; +using VideoResolver = MediaBrowser.Naming.Video.VideoResolver; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.IO; +using MediaBrowser.Model.Tasks; + +namespace Emby.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; + private readonly Func _userviewManager; + public bool IsScanRunning { get; private set; } + + /// + /// 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, Func userviewManager) + { + _logger = logger; + _taskManager = taskManager; + _userManager = userManager; + ConfigurationManager = configurationManager; + _userDataRepository = userDataRepository; + _libraryMonitorFactory = libraryMonitorFactory; + _fileSystem = fileSystem; + _providerManagerFactory = providerManagerFactory; + _userviewManager = userviewManager; + 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 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; + } + } + + /// + /// 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; + _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 newSeasonZeroName = ConfigurationManager.Configuration.SeasonZeroDisplayName; + var seasonZeroNameChanged = !string.Equals(_seasonZeroDisplayName, newSeasonZeroName, StringComparison.Ordinal); + var wizardChanged = config.IsStartupWizardCompleted != _wizardCompleted; + + RecordConfigurationValues(config); + + if (seasonZeroNameChanged || wizardChanged) + { + _taskManager.CancelIfRunningAndQueue(); + } + + if (seasonZeroNameChanged) + { + Task.Run(async () => + { + await UpdateSeasonZeroNames(newSeasonZeroName, CancellationToken.None).ConfigureAwait(false); + + }); + } + } + + /// + /// Updates the season zero names. + /// + /// The new name. + /// The cancellation token. + /// Task. + private async Task UpdateSeasonZeroNames(string newName, CancellationToken cancellationToken) + { + var seasons = GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Season).Name }, + Recursive = true, + IndexNumber = 0 + + }).Cast() + .Where(i => !string.Equals(i.Name, newName, StringComparison.Ordinal)) + .ToList(); + + foreach (var season in seasons) + { + season.Name = newName; + + try + { + await UpdateItem(season, ItemUpdateType.MetadataDownload, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error saving {0}", ex, season.Path); + } + } + } + + public void RegisterItem(BaseItem item) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + RegisterItem(item.Id, item); + } + + private void RegisterItem(Guid id, BaseItem item) + { + if (item is IItemByName) + { + if (!(item is MusicArtist)) + { + return; + } + } + + if (item.IsFolder) + { + if (!(item is ICollectionFolder) && !(item is UserView) && !(item is Channel) && !(item is AggregateFolder)) + { + if (item.SourceType != SourceType.Library) + { + return; + } + } + } + else + { + if (item is Photo) + { + return; + } + } + + LibraryItemsCache.AddOrUpdate(id, item, delegate { return item; }); + } + + public async Task DeleteItem(BaseItem item, DeleteOptions options) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + + _logger.Debug("Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", + item.GetType().Name, + item.Name ?? "Unknown name", + item.Path ?? string.Empty, + item.Id); + + var parent = item.Parent; + + var locationType = item.LocationType; + + var children = item.IsFolder + ? ((Folder)item).GetRecursiveChildren(false).ToList() + : new List(); + + foreach (var metadataPath in GetMetadataPaths(item, children)) + { + _logger.Debug("Deleting path {0}", metadataPath); + + try + { + _fileSystem.DeleteDirectory(metadataPath, true); + } + catch (IOException) + { + + } + 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 (_fileSystem.DirectoryExists(path)) + { + _logger.Debug("Deleting path {0}", path); + _fileSystem.DeleteDirectory(path, true); + } + else if (_fileSystem.FileExists(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) + { + parent.RemoveChild(item); + } + + 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. + /// 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.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("/", "\\"); + } + + if (!ConfigurationManager.Configuration.EnableCaseSensitiveItemIds) + { + key = key.ToLower(); + } + + key = type.FullName + key; + + return key.GetMD5(); + } + + /// + /// 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(FileSystemMetadata fileInfo, + Folder parent = null) + { + return ResolvePath(fileInfo, new DirectoryService(_logger, _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("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, + 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; + + 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.Values); + + fileSystemDictionary = paths.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, resolvers); + } + + private readonly List _ignoredPaths = new List(); + + public void RegisterIgnoredPath(string path) + { + lock (_ignoredPaths) + { + _ignoredPaths.Add(path); + } + } + public void UnRegisterIgnoredPath(string path) + { + lock (_ignoredPaths) + { + _ignoredPaths.Remove(path); + } + } + + public bool IgnoreFile(FileSystemMetadata file, BaseItem parent) + { + if (EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(file, parent))) + { + return true; + } + + //lock (_ignoredPaths) + { + if (_ignoredPaths.Contains(file.FullName, StringComparer.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } + + public IEnumerable 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.Info("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.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; + + _fileSystem.CreateDirectory(rootFolderPath); + + var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ?? (AggregateFolder)ResolvePath(_fileSystem.GetDirectoryInfo(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; + } + + if (folder.ParentId != rootFolder.Id) + { + folder.ParentId = rootFolder.Id; + var task = folder.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None); + Task.WaitAll(task); + } + + 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; + + _fileSystem.CreateDirectory(userRootPath); + + var tmpItem = GetItemById(GetNewItemId(userRootPath, typeof(UserRootFolder))) as UserRootFolder; + + if (tmpItem == null) + { + tmpItem = (UserRootFolder)ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath)); + } + + _userRootFolder = tmpItem; + } + } + } + + 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 + + var query = new InternalItemsQuery + { + Path = path, + IsFolder = isFolder, + SortBy = new[] { ItemSortBy.DateCreated }, + SortOrder = SortOrder.Descending, + Limit = 1 + }; + + return GetItemList(query) + .FirstOrDefault(); + } + + /// + /// Gets a Person + /// + /// The name. + /// Task{Person}. + public Person GetPerson(string name) + { + return CreateItemByName(Person.GetPath(name), name); + } + + /// + /// Gets a Studio + /// + /// The name. + /// Task{Studio}. + public Studio GetStudio(string name) + { + return CreateItemByName(Studio.GetPath(name), name); + } + + /// + /// Gets a Genre + /// + /// The name. + /// Task{Genre}. + public Genre GetGenre(string name) + { + return CreateItemByName(Genre.GetPath(name), name); + } + + /// + /// Gets the genre. + /// + /// The name. + /// Task{MusicGenre}. + public MusicGenre GetMusicGenre(string name) + { + return CreateItemByName(MusicGenre.GetPath(name), name); + } + + /// + /// Gets the game genre. + /// + /// The name. + /// Task{GameGenre}. + public GameGenre GetGameGenre(string name) + { + return CreateItemByName(GameGenre.GetPath(name), name); + } + + /// + /// 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."); + } + + var name = value.ToString(CultureInfo.InvariantCulture); + + return CreateItemByName(Year.GetPath(name), name); + } + + /// + /// Gets a Genre + /// + /// The name. + /// Task{Genre}. + public MusicArtist GetArtist(string name) + { + return CreateItemByName(MusicArtist.GetPath(name), name); + } + + private T CreateItemByName(string path, string name) + where T : BaseItem, new() + { + if (typeof(T) == typeof(MusicArtist)) + { + var existing = GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(T).Name }, + Name = name + + }).Cast() + .OrderBy(i => i.IsAccessedByName ? 1 : 0) + .Cast() + .FirstOrDefault(); + + if (existing != null) + { + return existing; + } + } + + var id = GetNewItemId(path, typeof(T)); + + var item = GetItemById(id) as T; + + if (item == null) + { + item = new T + { + Name = name, + Id = id, + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow, + Path = path + }; + + var task = CreateItem(item, CancellationToken.None); + Task.WaitAll(task); + } + + return item; + } + + public IEnumerable GetAlbumArtists(IEnumerable items) + { + var names = items + .SelectMany(i => i.AlbumArtists) + .DistinctNames() + .Select(i => + { + try + { + var artist = GetArtist(i); + + return artist; + } + catch + { + // Already logged at lower levels + return null; + } + }) + .Where(i => i != null); + + return names; + } + + public IEnumerable GetArtists(IEnumerable items) + { + var names = items + .SelectMany(i => i.AllArtists) + .DistinctNames() + .Select(i => + { + try + { + var artist = GetArtist(i); + + return artist; + } + catch + { + // Already logged at lower levels + return null; + } + }) + .Where(i => i != null); + + return names; + } + + /// + /// 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. + _fileSystem.CreateDirectory(ConfigurationManager.ApplicationPaths.PeoplePath); + + return new PeopleValidator(this, _logger, ConfigurationManager, _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.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) + { + IsScanRunning = true; + _libraryMonitorFactory().Stop(); + + try + { + await PerformLibraryValidation(progress, cancellationToken).ConfigureAwait(false); + } + finally + { + _libraryMonitorFactory().Start(); + IsScanRunning = false; + } + } + + 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(_fileSystem), recursive: false); + + progress.Report(1); + + var userRoot = GetUserRootFolder(); + + await userRoot.RefreshMetadata(cancellationToken).ConfigureAwait(false); + + await userRoot.ValidateChildren(new Progress(), cancellationToken, new MetadataRefreshOptions(_fileSystem), 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(_fileSystem), 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); + } + + /// + /// 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); + }); + + _logger.Debug("Running post-scan task {0}", task.GetType().Name); + + 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) + { + var topLibraryFolders = GetUserRootFolder().Children.ToList(); + + return _fileSystem.GetDirectoryPaths(path) + .Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders)); + } + + private VirtualFolderInfo GetVirtualFolderInfo(string dir, List allCollectionFolders) + { + var info = new VirtualFolderInfo + { + Name = Path.GetFileName(dir), + + Locations = _fileSystem.GetFilePaths(dir, false) + .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) + .Select(_fileSystem.ResolveShortcut) + .OrderBy(i => i) + .ToList(), + + 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"); + } + + if (libraryFolder != null) + { + info.ItemId = libraryFolder.Id.ToString("N"); + info.LibraryOptions = GetLibraryOptions(libraryFolder); + } + + return info; + } + + private string GetCollectionType(string path) + { + return _fileSystem.GetFiles(path, false) + .Where(i => string.Equals(i.Extension, ".collection", StringComparison.OrdinalIgnoreCase)) + .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); + + //_logger.Debug("GetitemById {0}", id); + + if (item != null) + { + RegisterItem(item); + } + + return item; + } + + public IEnumerable GetItemList(InternalItemsQuery query) + { + if (query.Recursive && query.ParentId.HasValue) + { + var parent = GetItemById(query.ParentId.Value); + if (parent != null) + { + SetTopParentIdsOrAncestors(query, new List { parent }); + query.ParentId = null; + } + } + + if (query.User != null) + { + AddUserToQuery(query, query.User); + } + + return ItemRepository.GetItemList(query); + } + + public IEnumerable GetItemList(InternalItemsQuery query, IEnumerable parentIds) + { + var parents = parentIds.Select(i => GetItemById(new Guid(i))).Where(i => i != null).ToList(); + + 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> GetStudios(InternalItemsQuery query) + { + if (query.User != null) + { + AddUserToQuery(query, query.User); + } + + SetTopParentOrAncestorIds(query); + return ItemRepository.GetStudios(query); + } + + public QueryResult> GetGenres(InternalItemsQuery query) + { + if (query.User != null) + { + AddUserToQuery(query, query.User); + } + + SetTopParentOrAncestorIds(query); + return ItemRepository.GetGenres(query); + } + + public QueryResult> GetGameGenres(InternalItemsQuery query) + { + if (query.User != null) + { + AddUserToQuery(query, query.User); + } + + SetTopParentOrAncestorIds(query); + return ItemRepository.GetGameGenres(query); + } + + public QueryResult> GetMusicGenres(InternalItemsQuery query) + { + if (query.User != null) + { + AddUserToQuery(query, query.User); + } + + SetTopParentOrAncestorIds(query); + return ItemRepository.GetMusicGenres(query); + } + + public QueryResult> GetAllArtists(InternalItemsQuery query) + { + if (query.User != null) + { + AddUserToQuery(query, query.User); + } + + SetTopParentOrAncestorIds(query); + return ItemRepository.GetAllArtists(query); + } + + public QueryResult> 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(new Guid(i))).ToList(); + + if (parents.All(i => + { + if (i is ICollectionFolder || i is UserView) + { + return true; + } + + //_logger.Debug("Query requires ancestor query due to type: " + i.GetType().Name); + return false; + + })) + { + // Optimize by querying against top level views + query.TopParentIds = parents.SelectMany(i => GetTopParentsForQuery(i, query.User)).Select(i => i.Id.ToString("N")).ToArray(); + query.AncestorIds = new string[] { }; + } + } + + public QueryResult> 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.HasValue) + { + var parent = GetItemById(query.ParentId.Value); + if (parent != null) + { + SetTopParentIdsOrAncestors(query, new List { parent }); + query.ParentId = null; + } + } + + if (query.User != null) + { + AddUserToQuery(query, query.User); + } + + if (query.EnableTotalRecordCount) + { + return ItemRepository.GetItems(query); + } + + return new QueryResult + { + Items = ItemRepository.GetItemList(query).ToArray() + }; + } + + private void SetTopParentIdsOrAncestors(InternalItemsQuery query, List parents) + { + if (parents.All(i => + { + if (i is ICollectionFolder || i is UserView) + { + return true; + } + + //_logger.Debug("Query requires ancestor query due to type: " + i.GetType().Name); + return false; + + })) + { + // Optimize by querying against top level views + query.TopParentIds = parents.SelectMany(i => GetTopParentsForQuery(i, query.User)).Select(i => i.Id.ToString("N")).ToArray(); + } + else + { + // We need to be able to query from any arbitrary ancestor up the tree + query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).Select(i => i.ToString("N")).ToArray(); + } + } + + private void AddUserToQuery(InternalItemsQuery query, User user) + { + if (query.AncestorIds.Length == 0 && + !query.ParentId.HasValue && + query.ChannelIds.Length == 0 && + query.TopParentIds.Length == 0 && + string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey) + && query.ItemIds.Length == 0) + { + var userViews = _userviewManager().GetUserViews(new UserViewQuery + { + UserId = user.Id.ToString("N"), + IncludeHidden = true + + }, CancellationToken.None).Result.ToList(); + + query.TopParentIds = userViews.SelectMany(i => GetTopParentsForQuery(i, user)).Select(i => i.Id.ToString("N")).ToArray(); + } + } + + private IEnumerable GetTopParentsForQuery(BaseItem item, User user) + { + var view = item as UserView; + + if (view != null) + { + if (string.Equals(view.ViewType, CollectionType.LiveTv)) + { + return new[] { view }; + } + if (string.Equals(view.ViewType, CollectionType.Channels)) + { + var channelResult = BaseItem.ChannelManager.GetChannelsInternal(new ChannelQuery + { + UserId = user.Id.ToString("N") + + }, CancellationToken.None).Result; + + return channelResult.Items; + } + + // Translate view into folders + if (view.DisplayParentId != Guid.Empty) + { + var displayParent = GetItemById(view.DisplayParentId); + if (displayParent != null) + { + return GetTopParentsForQuery(displayParent, user); + } + return new BaseItem[] { }; + } + if (view.ParentId != Guid.Empty) + { + var displayParent = GetItemById(view.ParentId); + if (displayParent != null) + { + return GetTopParentsForQuery(displayParent, user); + } + return new BaseItem[] { }; + } + + // Handle grouping + if (user != null && !string.IsNullOrWhiteSpace(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType)) + { + return user.RootFolder + .GetChildren(user, true) + .OfType() + .Where(i => string.IsNullOrWhiteSpace(i.CollectionType) || string.Equals(i.CollectionType, view.ViewType, StringComparison.OrdinalIgnoreCase)) + .Where(i => user.IsFolderGrouped(i.Id)) + .SelectMany(i => GetTopParentsForQuery(i, user)); + } + return new BaseItem[] { }; + } + + var collectionFolder = item as CollectionFolder; + if (collectionFolder != null) + { + return collectionFolder.GetPhysicalParents(); + } + + var topParent = item.GetTopParent(); + if (topParent != null) + { + return new[] { topParent }; + } + return new BaseItem[] { }; + } + + /// + /// 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; + } + else + { + return null; + } + } + } + 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) + { + RegisterItem(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); + + RegisterItem(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.GetParent() is AggregateFolder) && item.GetParent() != null) + { + item = item.GetParent(); + } + + 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 LibraryOptions GetLibraryOptions(BaseItem item) + { + var collectionFolder = item as CollectionFolder; + if (collectionFolder == null) + { + collectionFolder = GetCollectionFolders(item) + .OfType() + .FirstOrDefault(); + } + + var options = collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions(); + + if (options.SchemaVersion < 3) + { + options.SaveLocalMetadata = ConfigurationManager.Configuration.SaveLocalMeta; + options.EnableInternetProviders = ConfigurationManager.Configuration.EnableInternetProviders; + } + + if (options.SchemaVersion < 2) + { + var chapterOptions = ConfigurationManager.GetConfiguration("chapters"); + options.ExtractChapterImagesDuringLibraryScan = chapterOptions.ExtractDuringLibraryScan; + + if (collectionFolder != null) + { + if (string.Equals(collectionFolder.CollectionType, "movies", StringComparison.OrdinalIgnoreCase)) + { + options.EnableChapterImageExtraction = chapterOptions.EnableMovieChapterImageExtraction; + } + else if (string.Equals(collectionFolder.CollectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) + { + options.EnableChapterImageExtraction = chapterOptions.EnableEpisodeChapterImageExtraction; + } + } + } + + return options; + } + + 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.GetParents() + .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 && !string.IsNullOrWhiteSpace(i.Name) && _fileSystem.ContainsSubPath(i.Name, path))); + if (nameValuePair != null) + { + return nameValuePair.Value; + } + return null; + } + + private string GetTopFolderContentType(BaseItem item) + { + if (item == null) + { + return null; + } + + while (!(item.GetParent() is AggregateFolder) && item.GetParent() != null) + { + item = item.GetParent(); + } + + 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)); + } + + private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24); + //private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromMinutes(1); + + public Task GetNamedView(User user, + string name, + string viewType, + string sortName, + CancellationToken cancellationToken) + { + return GetNamedView(user, name, null, viewType, sortName, cancellationToken); + } + + public async Task GetNamedView(string name, + string viewType, + string sortName, + CancellationToken cancellationToken) + { + 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)) + { + _fileSystem.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) + { + refresh = DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; + } + + if (!refresh && item.DisplayParentId != Guid.Empty) + { + var displayParent = GetItemById(item.DisplayParentId); + refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; + } + + if (refresh) + { + await item.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None).ConfigureAwait(false); + _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem) + { + // Not sure why this is necessary but need to figure it out + // View images are not getting utilized without this + ForceSave = true + }); + } + + return item; + } + + public async Task GetNamedView(User user, + string name, + string parentId, + string viewType, + string sortName, + CancellationToken cancellationToken) + { + var idValues = "38_namedview_" + name + user.Id.ToString("N") + (parentId ?? string.Empty) + (viewType ?? string.Empty); + + var id = GetNewItemId(idValues, 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) + { + _fileSystem.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.DisplayParentId = new Guid(parentId); + } + + await CreateItem(item, cancellationToken).ConfigureAwait(false); + + isNew = true; + } + + var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; + + if (!refresh && item.DisplayParentId != Guid.Empty) + { + var displayParent = GetItemById(item.DisplayParentId); + refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; + } + + if (refresh) + { + _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem) + { + // Need to force save to increment DateLastSaved + ForceSave = true + }); + } + + return item; + } + + public async Task GetShadowView(BaseItem parent, + string viewType, + string sortName, + CancellationToken cancellationToken) + { + if (parent == null) + { + throw new ArgumentNullException("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) + { + _fileSystem.CreateDirectory(path); + + item = new UserView + { + Path = path, + Id = id, + DateCreated = DateTime.UtcNow, + Name = name, + ViewType = viewType, + ForcedSortName = sortName + }; + + item.DisplayParentId = parentId; + + await CreateItem(item, cancellationToken).ConfigureAwait(false); + + isNew = true; + } + + var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; + + if (!refresh && item.DisplayParentId != Guid.Empty) + { + var displayParent = GetItemById(item.DisplayParentId); + refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; + } + + if (refresh) + { + _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem) + { + // Need to force save to increment DateLastSaved + ForceSave = true + }); + } + + return item; + } + + public async Task GetNamedView(string name, + string parentId, + string viewType, + string sortName, + string uniqueId, + CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException("name"); + } + + var idValues = "37_namedview_" + name + (parentId ?? string.Empty) + (viewType ?? string.Empty); + if (!string.IsNullOrWhiteSpace(uniqueId)) + { + idValues += uniqueId; + } + + var id = GetNewItemId(idValues, 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) + { + _fileSystem.CreateDirectory(path); + + item = new UserView + { + Path = path, + Id = id, + DateCreated = DateTime.UtcNow, + Name = name, + ViewType = viewType, + ForcedSortName = sortName + }; + + if (!string.IsNullOrWhiteSpace(parentId)) + { + item.DisplayParentId = new Guid(parentId); + } + + await CreateItem(item, cancellationToken).ConfigureAwait(false); + + isNew = true; + } + + if (!string.Equals(viewType, item.ViewType, StringComparison.OrdinalIgnoreCase)) + { + item.ViewType = viewType; + await item.UpdateToRepository(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); + } + + var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; + + if (!refresh && item.DisplayParentId != Guid.Empty) + { + var displayParent = GetItemById(item.DisplayParentId); + refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; + } + + if (refresh) + { + _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem) + { + // Need to force save to increment DateLastSaved + ForceSave = true + }); + } + + return item; + } + + public bool IsVideoFile(string path, LibraryOptions libraryOptions) + { + var resolver = new VideoResolver(GetNamingOptions(libraryOptions), new PatternsLogger()); + 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(libraryOptions)); + return parser.IsAudioFile(path); + } + + public bool IsAudioFile(string path) + { + return IsAudioFile(path, new LibraryOptions()); + } + + 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 isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd || + episode.VideoType == VideoType.HdDvd; + + var locationType = episode.LocationType; + + var episodeInfo = locationType == LocationType.FileSystem || locationType == LocationType.Offline ? + resolver.Resolve(episode.Path, isFolder) : + new MediaBrowser.Naming.TV.EpisodeInfo(); + + if (episodeInfo == null) + { + episodeInfo = new MediaBrowser.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() + { + return GetNamingOptions(new LibraryOptions()); + } + + public NamingOptions GetNamingOptions(LibraryOptions libraryOptions) + { + var options = new ExtendedNamingOptions(); + + // These cause apps to have problems + options.AudioFileExtensions.Remove(".m3u"); + options.AudioFileExtensions.Remove(".wpl"); + + if (!libraryOptions.EnableArchiveMediaFiles) + { + options.AudioFileExtensions.Remove(".rar"); + options.AudioFileExtensions.Remove(".zip"); + } + + if (!libraryOptions.EnableArchiveMediaFiles) + { + 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