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.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Extensions;
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
    {
        /// 
        /// 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 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; }
        private readonly Func _libraryMonitorFactory;
        private readonly Func _providerManagerFactory;
        private readonly Func _userviewManager;
        public bool IsScanRunning { get; private set; }
        private IServerApplicationHost _appHost;
        /// 
        /// 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 logger.
        /// 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)
        {
            _logger = loggerFactory.CreateLogger(nameof(LibraryManager));
            _taskManager = taskManager;
            _userManager = userManager;
            ConfigurationManager = configurationManager;
            _userDataRepository = userDataRepository;
            _libraryMonitorFactory = libraryMonitorFactory;
            _fileSystem = fileSystem;
            _providerManagerFactory = providerManagerFactory;
            _userviewManager = userviewManager;
            _appHost = appHost;
            _libraryItemsCache = new ConcurrentDictionary();
            ConfigurationManager.ConfigurationUpdated += ConfigurationUpdated;
            RecordConfigurationValues(configurationManager.Configuration);
        }
        /// 
        /// Adds the parts.
        /// 
        /// The rules.
        /// The resolvers.
        /// The intro providers.
        /// The item comparers.
        /// The postscan 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.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;
            }
        }
        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.
        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 ICollectionFolder) && !(item is UserView) && !(item is Channel) && !(item is AggregateFolder))
                //{
                //    if (item.SourceType != SourceType.Library)
                //    {
                //        return;
                //    }
                //}
            }
            else
            {
                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))
            {
                _logger.LogDebug("Deleting path {0}", metadataPath);
                try
                {
                    Directory.Delete(metadataPath, true);
                }
                catch (IOException)
                {
                }
                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().ToList())
                {
                    if (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))
            {
                // 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)
        {
            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(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 = new FileSystemMetadata[] { };
                    }
                    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)
        {
            if (EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(file, parent)))
            {
                return true;
            }
            return false;
        }
        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;
                        Directory.CreateDirectory(userRootPath);
                        var tmpItem = GetItemById(GetNewItemId(userRootPath, typeof(UserRootFolder))) as UserRootFolder;
                        if (tmpItem == null)
                        {
                            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;
                    }
                }
            }
            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));
            }
            //_logger.LogInformation("FindByPath {0}", path);
            var query = new InternalItemsQuery
            {
                Path = path,
                IsFolder = isFolder,
                OrderBy = new[] { ItemSortBy.DateCreated }.Select(i => new ValueTuple(i, SortOrder.Descending)).ToArray(),
                Limit = 1,
                DtoOptions = new DtoOptions(true)
            };
            return GetItemList(query)
                .FirstOrDefault();
        }
        /// 
        /// Gets a Person
        /// 
        /// The name.
        /// Task{Person}.
        public Person GetPerson(string name)
        {
            return CreateItemByName(Person.GetPath, name, new DtoOptions(true));
        }
        /// 
        /// Gets a 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 a Genre
        /// 
        /// The name.
        /// Task{Genre}.
        public Genre GetGenre(string name)
        {
            return CreateItemByName(Genre.GetPath, name, new DtoOptions(true));
        }
        /// 
        /// Gets the genre.
        /// 
        /// The name.
        /// Task{MusicGenre}.
        public MusicGenre GetMusicGenre(string name)
        {
            return CreateItemByName(MusicGenre.GetPath, name, new DtoOptions(true));
        }
        /// 
        /// Gets a 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)
        {
            var rootChildren = RootFolder.Children.ToList();
            rootChildren = GetUserRootFolder().Children.ToList();
            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(_logger, _fileSystem)), recursive: false);
            await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false);
            await GetUserRootFolder().ValidateChildren(new SimpleProgress(), cancellationToken, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), recursive: false).ConfigureAwait(false);
            // Quickly scan CollectionFolders for changes
            foreach (var folder in GetUserRootFolder().Children.OfType().ToList())
            {
                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 * .96));
            // Now validate the entire media library
            await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), recursive: true).ConfigureAwait(false);
            progress.Report(96);
            innerProgress = new ActionableProgress();
            innerProgress.RegisterAction(pct => progress.Report(96 + (pct * .04)));
            // 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 = 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 postscan 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)
        {
            var topLibraryFolders = GetUserRootFolder().Children.ToList();
            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");
            }
            if (libraryFolder != null)
            {
                info.ItemId = libraryFolder.Id.ToString("N");
                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(i => Path.GetFileNameWithoutExtension(i))
                .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(nameof(id), "Guid can't be empty");
            }
            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.Equals(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 =>
            {
                if (i is ICollectionFolder || i is UserView)
                {
                    return true;
                }
                //_logger.LogDebug("Query requires ancestor query due to type: " + i.GetType().Name);
                return false;
            }))
            {
                // 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.ToArray()
            };
        }
        private void SetTopParentIdsOrAncestors(InternalItemsQuery query, List parents)
        {
            if (parents.All(i =>
            {
                if (i is ICollectionFolder || i is UserView)
                {
                    return true;
                }
                //_logger.LogDebug("Query requires ancestor query due to type: " + i.GetType().Name);
                return false;
            }))
            {
                // 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)
        {
            var view = item as UserView;
            if (view != null)
            {
                if (string.Equals(view.ViewType, CollectionType.LiveTv))
                {
                    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();
            }
            var collectionFolder = item as CollectionFolder;
            if (collectionFolder != null)
            {
                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 (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 parent item.
        /// Task.
        public void CreateItem(BaseItem item, BaseItem parent)
        {
            CreateItems(new[] { item }, parent, CancellationToken.None);
        }
        /// 
        /// Creates the items.
        /// 
        /// The items.
        /// The cancellation token.
        /// Task.
        public void CreateItems(IEnumerable items, BaseItem parent, CancellationToken cancellationToken)
        {
            ItemRepository.SaveItems(items, cancellationToken);
            foreach (var item in items)
            {
                RegisterItem(item);
            }
            if (ItemAdded != null)
            {
                foreach (var item in items)
                {
                    // 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)
        {
            foreach (var item in items)
            {
                if (item.IsFileProtocol)
                {
                    _providerManagerFactory().SaveMetadata(item, updateReason);
                }
                item.DateLastSaved = DateTime.UtcNow;
                RegisterItem(item);
            }
            //var logName = item.LocationType == LocationType.Remote ? item.Name ?? item.Path : item.Path ?? item.Name;
            //_logger.LogDebug("Saving {0} to database.", logName);
            ItemRepository.SaveItems(items, cancellationToken);
            if (ItemUpdated != null)
            {
                foreach (var item in items)
                {
                    // 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 update reason.
        /// The cancellation token.
        /// Task.
        public void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
        {
            UpdateItems(new [] { item }, parent, updateReason, cancellationToken);
        }
        /// 
        /// Reports the item removed.
        /// 
        /// The 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)
        {
            var collectionFolder = item as CollectionFolder;
            if (collectionFolder == null)
            {
                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)
        {
            var 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 => _fileSystem.AreEqual(i.Name, path) || (inherit && !string.IsNullOrEmpty(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.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);
        //private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromMinutes(1);
        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(_logger, _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");
            var idValues = "38_namedview_" + name + user.Id.ToString("N") + (parentIdString ?? 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)
            {
                Directory.CreateDirectory(path);
                item = new UserView
                {
                    Path = path,
                    Id = id,
                    DateCreated = DateTime.UtcNow,
                    Name = name,
                    ViewType = viewType,
                    ForcedSortName = sortName,
                    UserId = user.Id
                };
                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(_logger, _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(_logger, _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");
            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"));
            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(_logger, _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 new SeasonPathParser(GetNamingOptions()).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();
            }
            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;
                }
                if (episode.ParentIndexNumber.HasValue)
                {
                    changed = true;
                }
            }
            return changed;
        }
        public NamingOptions GetNamingOptions()
        {
            return GetNamingOptionsInternal();
        }
        private NamingOptions _namingOptions;
        private string[] _videoFileExtensions;
        private NamingOptions GetNamingOptionsInternal()
        {
            if (_namingOptions == null)
            {
                var options = new NamingOptions();
                _namingOptions = options;
                _videoFileExtensions = _namingOptions.VideoFileExtensions.ToArray();
            }
            return _namingOptions;
        }
        public ItemLookupInfo ParseName(string name)
        {
            var resolver = new VideoResolver(GetNamingOptions());
            var result = resolver.CleanDateTime(name);
            var cleanName = resolver.CleanString(result.Name);
            return new ItemLookupInfo
            {
                Name = cleanName.Name,
                Year = result.Year
            };
        }
        public IEnumerable