< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.Library.LibraryManager
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/Library/LibraryManager.cs
Line coverage
39%
Covered lines: 415
Uncovered lines: 631
Coverable lines: 1046
Total lines: 3190
Line coverage: 39.6%
Branch coverage
33%
Covered branches: 193
Total branches: 574
Branch coverage: 33.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_RootFolder()100%44100%
get_LibraryMonitor()100%11100%
get_ProviderManager()100%11100%
get_UserViewManager()100%11100%
AddParts(...)100%11100%
RecordConfigurationValues(...)100%11100%
ConfigurationUpdated(...)100%22100%
RegisterItem(...)50%18.781055.55%
DeleteItem(...)100%210%
DeleteItem(...)0%620%
DeleteItem(...)23.91%928.024625.3%
GetMetadataPaths(...)100%11100%
ResolveItem(...)100%44100%
Resolve(...)100%1.22140%
GetNewItemId(...)100%11100%
GetNewItemIdInternal(...)100%66100%
ResolvePath(...)100%22100%
ResolvePath(...)70%26.882074.19%
IgnoreFile(...)100%11100%
NormalizeRootPathList(...)50%2291.66%
ShouldResolvePathContents(...)100%11100%
ResolvePaths(...)100%11100%
ResolvePaths(...)58.33%22.421258.33%
CreateRootFolder()68.75%16.831685.18%
GetUserRootFolder()80%10.911079.16%
FindByPath(...)100%210%
GetPerson(...)50%2.03280%
GetStudio(...)100%210%
GetStudioId(...)100%210%
GetGenreId(...)100%210%
GetMusicGenreId(...)100%210%
GetGenre(...)100%210%
GetMusicGenre(...)100%210%
GetYear(...)0%620%
GetArtist(...)100%210%
GetArtist(...)100%210%
CreateItemByName(...)0%7280%
GetItemByNameId(...)100%11100%
ValidatePeopleAsync(...)100%210%
ValidateMediaLibrary(...)100%11100%
GetVirtualFolders()100%11100%
GetVirtualFolders(...)100%22100%
GetVirtualFolderInfo(...)70%101097.14%
GetCollectionType(...)50%6450%
GetItemById(...)66.66%6.56675%
GetItemById(...)100%22100%
GetItemById(...)50%22100%
GetItemById(...)100%22100%
GetItemList(...)100%1010100%
GetItemList(...)100%11100%
GetCount(...)0%7280%
GetItemList(...)0%4260%
QueryItems(...)0%2040%
GetItemIds(...)50%2.15266.66%
GetStudios(...)0%620%
GetGenres(...)0%620%
GetMusicGenres(...)0%620%
GetAllArtists(...)0%620%
GetArtists(...)0%620%
SetTopParentOrAncestorIds(...)0%210140%
GetAlbumArtists(...)0%620%
GetItemsResult(...)100%1010100%
SetTopParentIdsOrAncestors(...)83.33%6.05688.88%
AddUserToQuery(...)100%1616100%
GetTopParentIdsForQuery(...)8.33%434.042410.71%
ResolveIntro(...)0%110100%
Sort(...)57.14%33.921453.33%
Sort(...)0%210140%
GetComparer(...)50%2.98237.5%
CreateItem(...)100%210%
CreateItems(...)90%10.461083.33%
ImageNeedsRefresh(...)0%156120%
UpdateItemAsync(...)100%11100%
ReportItemRemoved(...)100%2.05276.92%
RetrieveItem(...)100%11100%
GetCollectionFolders(...)100%11100%
GetCollectionFolders(...)80%10.61081.81%
GetCollectionFoldersInternal(...)100%11100%
GetLibraryOptions(...)75%44100%
GetContentType(...)50%4.37471.42%
GetInheritedContentType(...)50%2.02283.33%
GetConfiguredContentType(...)100%210%
GetConfiguredContentType(...)100%210%
GetConfiguredContentType(...)50%2.15266.66%
GetContentTypeOverride(...)50%4.05485.71%
GetTopFolderContentType(...)50%11.08863.63%
GetNamedView(...)100%210%
GetNamedView(...)0%4260%
GetNamedView(...)0%420200%
GetShadowView(...)0%272160%
GetNamedView(...)0%600240%
GetParentItem(...)33.33%8.3660%
QueueLibraryScan()100%210%
GetSeasonNumberFromPath(...)100%210%
FillMissingEpisodeNumbersFromPath(...)0%4422660%
ParseName(...)0%620%
GetPathAfterNetworkSubstitution(...)25%6450%
GetPeople(...)100%210%
GetPeople(...)50%10.75425%
GetPeopleItems(...)100%210%
GetPeopleNames(...)100%210%
UpdatePeople(...)100%210%
StartScanInBackground()100%11100%
AddMediaPath(...)100%1.13150%
AddMediaPathInternal(...)25%36.32823.8%
UpdateMediaPath(...)100%210%
SyncLibraryOptionsToLocations(...)0%7280%
RemoveContentTypeOverrides(...)0%210140%
RemoveMediaPath(...)25%8.74433.33%
ItemIsVisible(...)66.66%6.29680%

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/Library/LibraryManager.cs

#LineLine coverage
 1#pragma warning disable CS1591
 2#pragma warning disable CA5394
 3
 4using System;
 5using System.Collections.Concurrent;
 6using System.Collections.Generic;
 7using System.Globalization;
 8using System.IO;
 9using System.Linq;
 10using System.Net;
 11using System.Net.Http;
 12using System.Threading;
 13using System.Threading.Tasks;
 14using Emby.Naming.Common;
 15using Emby.Naming.TV;
 16using Emby.Server.Implementations.Library.Resolvers;
 17using Emby.Server.Implementations.Library.Validators;
 18using Emby.Server.Implementations.Playlists;
 19using Emby.Server.Implementations.ScheduledTasks.Tasks;
 20using Emby.Server.Implementations.Sorting;
 21using Jellyfin.Data.Entities;
 22using Jellyfin.Data.Enums;
 23using Jellyfin.Extensions;
 24using MediaBrowser.Common.Extensions;
 25using MediaBrowser.Controller;
 26using MediaBrowser.Controller.Configuration;
 27using MediaBrowser.Controller.Drawing;
 28using MediaBrowser.Controller.Dto;
 29using MediaBrowser.Controller.Entities;
 30using MediaBrowser.Controller.Entities.Audio;
 31using MediaBrowser.Controller.IO;
 32using MediaBrowser.Controller.Library;
 33using MediaBrowser.Controller.LiveTv;
 34using MediaBrowser.Controller.MediaEncoding;
 35using MediaBrowser.Controller.Persistence;
 36using MediaBrowser.Controller.Providers;
 37using MediaBrowser.Controller.Resolvers;
 38using MediaBrowser.Controller.Sorting;
 39using MediaBrowser.Model.Configuration;
 40using MediaBrowser.Model.Dlna;
 41using MediaBrowser.Model.Drawing;
 42using MediaBrowser.Model.Dto;
 43using MediaBrowser.Model.Entities;
 44using MediaBrowser.Model.IO;
 45using MediaBrowser.Model.Library;
 46using MediaBrowser.Model.Querying;
 47using MediaBrowser.Model.Tasks;
 48using Microsoft.Extensions.Logging;
 49using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 50using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
 51using Genre = MediaBrowser.Controller.Entities.Genre;
 52using Person = MediaBrowser.Controller.Entities.Person;
 53using VideoResolver = Emby.Naming.Video.VideoResolver;
 54
 55namespace Emby.Server.Implementations.Library
 56{
 57    /// <summary>
 58    /// Class LibraryManager.
 59    /// </summary>
 60    public class LibraryManager : ILibraryManager
 61    {
 62        private const string ShortcutFileExtension = ".mblink";
 63
 64        private readonly ILogger<LibraryManager> _logger;
 65        private readonly ConcurrentDictionary<Guid, BaseItem> _cache;
 66        private readonly ITaskManager _taskManager;
 67        private readonly IUserManager _userManager;
 68        private readonly IUserDataManager _userDataRepository;
 69        private readonly IServerConfigurationManager _configurationManager;
 70        private readonly Lazy<ILibraryMonitor> _libraryMonitorFactory;
 71        private readonly Lazy<IProviderManager> _providerManagerFactory;
 72        private readonly Lazy<IUserViewManager> _userviewManagerFactory;
 73        private readonly IServerApplicationHost _appHost;
 74        private readonly IMediaEncoder _mediaEncoder;
 75        private readonly IFileSystem _fileSystem;
 76        private readonly IItemRepository _itemRepository;
 77        private readonly IImageProcessor _imageProcessor;
 78        private readonly NamingOptions _namingOptions;
 79        private readonly ExtraResolver _extraResolver;
 80
 81        /// <summary>
 82        /// The _root folder sync lock.
 83        /// </summary>
 2984        private readonly object _rootFolderSyncLock = new object();
 2985        private readonly object _userRootFolderSyncLock = new object();
 86
 2987        private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
 88
 89        /// <summary>
 90        /// The _root folder.
 91        /// </summary>
 92        private volatile AggregateFolder? _rootFolder;
 93        private volatile UserRootFolder? _userRootFolder;
 94
 95        private bool _wizardCompleted;
 96
 97        /// <summary>
 98        /// Initializes a new instance of the <see cref="LibraryManager" /> class.
 99        /// </summary>
 100        /// <param name="appHost">The application host.</param>
 101        /// <param name="loggerFactory">The logger factory.</param>
 102        /// <param name="taskManager">The task manager.</param>
 103        /// <param name="userManager">The user manager.</param>
 104        /// <param name="configurationManager">The configuration manager.</param>
 105        /// <param name="userDataRepository">The user data repository.</param>
 106        /// <param name="libraryMonitorFactory">The library monitor.</param>
 107        /// <param name="fileSystem">The file system.</param>
 108        /// <param name="providerManagerFactory">The provider manager.</param>
 109        /// <param name="userviewManagerFactory">The userview manager.</param>
 110        /// <param name="mediaEncoder">The media encoder.</param>
 111        /// <param name="itemRepository">The item repository.</param>
 112        /// <param name="imageProcessor">The image processor.</param>
 113        /// <param name="namingOptions">The naming options.</param>
 114        /// <param name="directoryService">The directory service.</param>
 115        public LibraryManager(
 116            IServerApplicationHost appHost,
 117            ILoggerFactory loggerFactory,
 118            ITaskManager taskManager,
 119            IUserManager userManager,
 120            IServerConfigurationManager configurationManager,
 121            IUserDataManager userDataRepository,
 122            Lazy<ILibraryMonitor> libraryMonitorFactory,
 123            IFileSystem fileSystem,
 124            Lazy<IProviderManager> providerManagerFactory,
 125            Lazy<IUserViewManager> userviewManagerFactory,
 126            IMediaEncoder mediaEncoder,
 127            IItemRepository itemRepository,
 128            IImageProcessor imageProcessor,
 129            NamingOptions namingOptions,
 130            IDirectoryService directoryService)
 131        {
 29132            _appHost = appHost;
 29133            _logger = loggerFactory.CreateLogger<LibraryManager>();
 29134            _taskManager = taskManager;
 29135            _userManager = userManager;
 29136            _configurationManager = configurationManager;
 29137            _userDataRepository = userDataRepository;
 29138            _libraryMonitorFactory = libraryMonitorFactory;
 29139            _fileSystem = fileSystem;
 29140            _providerManagerFactory = providerManagerFactory;
 29141            _userviewManagerFactory = userviewManagerFactory;
 29142            _mediaEncoder = mediaEncoder;
 29143            _itemRepository = itemRepository;
 29144            _imageProcessor = imageProcessor;
 29145            _cache = new ConcurrentDictionary<Guid, BaseItem>();
 29146            _namingOptions = namingOptions;
 147
 29148            _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryServ
 149
 29150            _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
 151
 29152            RecordConfigurationValues(configurationManager.Configuration);
 29153        }
 154
 155        /// <summary>
 156        /// Occurs when [item added].
 157        /// </summary>
 158        public event EventHandler<ItemChangeEventArgs>? ItemAdded;
 159
 160        /// <summary>
 161        /// Occurs when [item updated].
 162        /// </summary>
 163        public event EventHandler<ItemChangeEventArgs>? ItemUpdated;
 164
 165        /// <summary>
 166        /// Occurs when [item removed].
 167        /// </summary>
 168        public event EventHandler<ItemChangeEventArgs>? ItemRemoved;
 169
 170        /// <summary>
 171        /// Gets the root folder.
 172        /// </summary>
 173        /// <value>The root folder.</value>
 174        public AggregateFolder RootFolder
 175        {
 176            get
 177            {
 148178                if (_rootFolder is null)
 179                {
 22180                    lock (_rootFolderSyncLock)
 181                    {
 22182                        _rootFolder ??= CreateRootFolder();
 22183                    }
 184                }
 185
 148186                return _rootFolder;
 187            }
 188        }
 189
 40190        private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
 191
 59192        private IProviderManager ProviderManager => _providerManagerFactory.Value;
 193
 1194        private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
 195
 196        /// <summary>
 197        /// Gets or sets the postscan tasks.
 198        /// </summary>
 199        /// <value>The postscan tasks.</value>
 200        private ILibraryPostScanTask[] PostscanTasks { get; set; } = Array.Empty<ILibraryPostScanTask>();
 201
 202        /// <summary>
 203        /// Gets or sets the intro providers.
 204        /// </summary>
 205        /// <value>The intro providers.</value>
 206        private IIntroProvider[] IntroProviders { get; set; } = Array.Empty<IIntroProvider>();
 207
 208        /// <summary>
 209        /// Gets or sets the list of entity resolution ignore rules.
 210        /// </summary>
 211        /// <value>The entity resolution ignore rules.</value>
 212        private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = Array.Empty<IResolverIgnoreRule>();
 213
 214        /// <summary>
 215        /// Gets or sets the list of currently registered entity resolvers.
 216        /// </summary>
 217        /// <value>The entity resolvers enumerable.</value>
 218        private IItemResolver[] EntityResolvers { get; set; } = Array.Empty<IItemResolver>();
 219
 220        private IMultiItemResolver[] MultiItemResolvers { get; set; } = Array.Empty<IMultiItemResolver>();
 221
 222        /// <summary>
 223        /// Gets or sets the comparers.
 224        /// </summary>
 225        /// <value>The comparers.</value>
 226        private IBaseItemComparer[] Comparers { get; set; } = Array.Empty<IBaseItemComparer>();
 227
 228        public bool IsScanRunning { get; private set; }
 229
 230        /// <summary>
 231        /// Adds the parts.
 232        /// </summary>
 233        /// <param name="rules">The rules.</param>
 234        /// <param name="resolvers">The resolvers.</param>
 235        /// <param name="introProviders">The intro providers.</param>
 236        /// <param name="itemComparers">The item comparers.</param>
 237        /// <param name="postscanTasks">The post scan tasks.</param>
 238        public void AddParts(
 239            IEnumerable<IResolverIgnoreRule> rules,
 240            IEnumerable<IItemResolver> resolvers,
 241            IEnumerable<IIntroProvider> introProviders,
 242            IEnumerable<IBaseItemComparer> itemComparers,
 243            IEnumerable<ILibraryPostScanTask> postscanTasks)
 244        {
 29245            EntityResolutionIgnoreRules = rules.ToArray();
 29246            EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
 29247            MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
 29248            IntroProviders = introProviders.ToArray();
 29249            Comparers = itemComparers.ToArray();
 29250            PostscanTasks = postscanTasks.ToArray();
 29251        }
 252
 253        /// <summary>
 254        /// Records the configuration values.
 255        /// </summary>
 256        /// <param name="configuration">The configuration.</param>
 257        private void RecordConfigurationValues(ServerConfiguration configuration)
 258        {
 69259            _wizardCompleted = configuration.IsStartupWizardCompleted;
 69260        }
 261
 262        /// <summary>
 263        /// Configurations the updated.
 264        /// </summary>
 265        /// <param name="sender">The sender.</param>
 266        /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
 267        private void ConfigurationUpdated(object? sender, EventArgs e)
 268        {
 40269            var config = _configurationManager.Configuration;
 270
 40271            var wizardChanged = config.IsStartupWizardCompleted != _wizardCompleted;
 272
 40273            RecordConfigurationValues(config);
 274
 40275            if (wizardChanged)
 276            {
 17277                _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
 278            }
 40279        }
 280
 281        public void RegisterItem(BaseItem item)
 282        {
 109283            ArgumentNullException.ThrowIfNull(item);
 284
 109285            if (item is IItemByName)
 286            {
 0287                if (item is not MusicArtist)
 288                {
 0289                    return;
 290                }
 291            }
 109292            else if (!item.IsFolder)
 293            {
 0294                if (item is not Video && item is not LiveTvChannel)
 295                {
 0296                    return;
 297                }
 298            }
 299
 109300            _cache[item.Id] = item;
 109301        }
 302
 303        public void DeleteItem(BaseItem item, DeleteOptions options)
 304        {
 0305            DeleteItem(item, options, false);
 0306        }
 307
 308        public void DeleteItem(BaseItem item, DeleteOptions options, bool notifyParentItem)
 309        {
 0310            ArgumentNullException.ThrowIfNull(item);
 311
 0312            var parent = item.GetOwner() ?? item.GetParent();
 313
 0314            DeleteItem(item, options, parent, notifyParentItem);
 0315        }
 316
 317        public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
 318        {
 2319            ArgumentNullException.ThrowIfNull(item);
 320
 2321            if (item.SourceType == SourceType.Channel)
 322            {
 0323                if (options.DeleteFromExternalProvider)
 324                {
 325                    try
 326                    {
 0327                        BaseItem.ChannelManager.DeleteItem(item).GetAwaiter().GetResult();
 0328                    }
 0329                    catch (ArgumentException)
 330                    {
 331                        // channel no longer installed
 0332                    }
 333                }
 334
 0335                options.DeleteFileLocation = false;
 336            }
 337
 2338            if (item is LiveTvProgram)
 339            {
 0340                _logger.LogDebug(
 0341                    "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0342                    item.GetType().Name,
 0343                    item.Name ?? "Unknown name",
 0344                    item.Path ?? string.Empty,
 0345                    item.Id);
 346            }
 347            else
 348            {
 2349                _logger.LogInformation(
 2350                    "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 2351                    item.GetType().Name,
 2352                    item.Name ?? "Unknown name",
 2353                    item.Path ?? string.Empty,
 2354                    item.Id);
 355            }
 356
 2357            var children = item.IsFolder
 2358                ? ((Folder)item).GetRecursiveChildren(false)
 2359                : Array.Empty<BaseItem>();
 360
 8361            foreach (var metadataPath in GetMetadataPaths(item, children))
 362            {
 2363                if (!Directory.Exists(metadataPath))
 364                {
 365                    continue;
 366                }
 367
 0368                _logger.LogDebug(
 0369                    "Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0370                    item.GetType().Name,
 0371                    item.Name ?? "Unknown name",
 0372                    metadataPath,
 0373                    item.Id);
 374
 375                try
 376                {
 0377                    Directory.Delete(metadataPath, true);
 0378                }
 0379                catch (Exception ex)
 380                {
 0381                    _logger.LogError(ex, "Error deleting {MetadataPath}", metadataPath);
 0382                }
 383            }
 384
 2385            if (options.DeleteFileLocation && item.IsFileProtocol)
 386            {
 387                // Assume only the first is required
 388                // Add this flag to GetDeletePaths if required in the future
 0389                var isRequiredForDelete = true;
 390
 0391                foreach (var fileSystemInfo in item.GetDeletePaths())
 392                {
 0393                    if (Directory.Exists(fileSystemInfo.FullName) || File.Exists(fileSystemInfo.FullName))
 394                    {
 395                        try
 396                        {
 0397                            _logger.LogInformation(
 0398                                "Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0399                                item.GetType().Name,
 0400                                item.Name ?? "Unknown name",
 0401                                fileSystemInfo.FullName,
 0402                                item.Id);
 403
 0404                            if (fileSystemInfo.IsDirectory)
 405                            {
 0406                                Directory.Delete(fileSystemInfo.FullName, true);
 407                            }
 408                            else
 409                            {
 0410                                File.Delete(fileSystemInfo.FullName);
 411                            }
 0412                        }
 0413                        catch (DirectoryNotFoundException)
 414                        {
 0415                            _logger.LogInformation(
 0416                                "Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Pa
 0417                                item.GetType().Name,
 0418                                item.Name ?? "Unknown name",
 0419                                fileSystemInfo.FullName,
 0420                                item.Id);
 0421                        }
 0422                        catch (FileNotFoundException)
 423                        {
 0424                            _logger.LogInformation(
 0425                                "File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, 
 0426                                item.GetType().Name,
 0427                                item.Name ?? "Unknown name",
 0428                                fileSystemInfo.FullName,
 0429                                item.Id);
 0430                        }
 0431                        catch (IOException)
 432                        {
 0433                            if (isRequiredForDelete)
 434                            {
 0435                                throw;
 436                            }
 0437                        }
 0438                        catch (UnauthorizedAccessException)
 439                        {
 0440                            if (isRequiredForDelete)
 441                            {
 0442                                throw;
 443                            }
 0444                        }
 445                    }
 446
 0447                    isRequiredForDelete = false;
 448                }
 449            }
 450
 2451            item.SetParent(null);
 452
 2453            _itemRepository.DeleteItem(item.Id);
 4454            foreach (var child in children)
 455            {
 0456                _itemRepository.DeleteItem(child.Id);
 457            }
 458
 2459            _cache.TryRemove(item.Id, out _);
 460
 2461            ReportItemRemoved(item, parent);
 2462        }
 463
 464        private static List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
 465        {
 2466            var list = new List<string>
 2467            {
 2468                item.GetInternalMetadataPath()
 2469            };
 470
 2471            list.AddRange(children.Select(i => i.GetInternalMetadataPath()));
 472
 2473            return list;
 474        }
 475
 476        /// <summary>
 477        /// Resolves the item.
 478        /// </summary>
 479        /// <param name="args">The args.</param>
 480        /// <param name="resolvers">The resolvers.</param>
 481        /// <returns>BaseItem.</returns>
 482        private BaseItem? ResolveItem(ItemResolveArgs args, IItemResolver[]? resolvers)
 483        {
 66484            var item = (resolvers ?? EntityResolvers).Select(r => Resolve(args, r))
 66485                .FirstOrDefault(i => i is not null);
 486
 66487            if (item is not null)
 488            {
 63489                ResolverHelper.SetInitialItemValues(item, args, _fileSystem, this);
 490            }
 491
 66492            return item;
 493        }
 494
 495        private BaseItem? Resolve(ItemResolveArgs args, IItemResolver resolver)
 496        {
 497            try
 498            {
 197499                return resolver.ResolvePath(args);
 500            }
 0501            catch (Exception ex)
 502            {
 0503                _logger.LogError(ex, "Error in {Resolver} resolving {Path}", resolver.GetType().Name, args.Path);
 0504                return null;
 505            }
 197506        }
 507
 508        public Guid GetNewItemId(string key, Type type)
 509        {
 129510            return GetNewItemIdInternal(key, type, false);
 511        }
 512
 513        private Guid GetNewItemIdInternal(string key, Type type, bool forceCaseInsensitive)
 514        {
 130515            ArgumentException.ThrowIfNullOrEmpty(key);
 130516            ArgumentNullException.ThrowIfNull(type);
 517
 130518            string programDataPath = _configurationManager.ApplicationPaths.ProgramDataPath;
 130519            if (key.StartsWith(programDataPath, StringComparison.Ordinal))
 520            {
 521                // Try to normalize paths located underneath program-data in an attempt to make them more portable
 113522                key = key.Substring(programDataPath.Length)
 113523                    .TrimStart('/', '\\')
 113524                    .Replace('/', '\\');
 525            }
 526
 130527            if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
 528            {
 1529                key = key.ToLowerInvariant();
 530            }
 531
 130532            key = type.FullName + key;
 533
 130534            return key.GetMD5();
 535        }
 536
 537        public BaseItem? ResolvePath(FileSystemMetadata fileInfo, Folder? parent = null, IDirectoryService? directorySer
 44538            => ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent);
 539
 540        private BaseItem? ResolvePath(
 541            FileSystemMetadata fileInfo,
 542            IDirectoryService directoryService,
 543            IItemResolver[]? resolvers,
 544            Folder? parent = null,
 545            CollectionType? collectionType = null,
 546            LibraryOptions? libraryOptions = null)
 547        {
 66548            ArgumentNullException.ThrowIfNull(fileInfo);
 549
 66550            var fullPath = fileInfo.FullName;
 551
 66552            if (collectionType is null && parent is not null)
 553            {
 5554                collectionType = GetContentTypeOverride(fullPath, true);
 555            }
 556
 66557            var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, this)
 66558            {
 66559                Parent = parent,
 66560                FileInfo = fileInfo,
 66561                CollectionType = collectionType,
 66562                LibraryOptions = libraryOptions
 66563            };
 564
 565            // Return null if ignore rules deem that we should do so
 66566            if (IgnoreFile(args.FileInfo, args.Parent))
 567            {
 0568                return null;
 569            }
 570
 571            // Gather child folder and files
 66572            if (args.IsDirectory)
 573            {
 46574                var isPhysicalRoot = args.IsPhysicalRoot;
 575
 576                // When resolving the root, we need it's grandchildren (children of user views)
 46577                var flattenFolderDepth = isPhysicalRoot ? 2 : 0;
 578
 579                FileSystemMetadata[] files;
 46580                var isVf = args.IsVf;
 581
 582                try
 583                {
 46584                    files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, _fileSystem, _appHost, _l
 46585                }
 0586                catch (Exception ex)
 587                {
 0588                    if (parent is not null && parent.IsPhysicalRoot)
 589                    {
 0590                        _logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPh
 591
 0592                        files = Array.Empty<FileSystemMetadata>();
 593                    }
 594                    else
 595                    {
 0596                        throw;
 597                    }
 0598                }
 599
 600                // Need to remove subpaths that may have been resolved from shortcuts
 601                // Example: if \\server\movies exists, then strip out \\server\movies\action
 46602                if (isPhysicalRoot)
 603                {
 22604                    files = NormalizeRootPathList(files).ToArray();
 605                }
 606
 46607                args.FileSystemChildren = files;
 608            }
 609
 610            // Check to see if we should resolve based on our contents
 66611            if (args.IsDirectory && !ShouldResolvePathContents(args))
 612            {
 0613                return null;
 614            }
 615
 66616            return ResolveItem(args, resolvers);
 617        }
 618
 619        public bool IgnoreFile(FileSystemMetadata file, BaseItem? parent)
 71620            => EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(file, parent));
 621
 622        public List<FileSystemMetadata> NormalizeRootPathList(IEnumerable<FileSystemMetadata> paths)
 623        {
 82624            var originalList = paths.ToList();
 625
 82626            var list = originalList.Where(i => i.IsDirectory)
 82627                .Select(i => Path.TrimEndingDirectorySeparator(i.FullName))
 82628                .Distinct(StringComparer.OrdinalIgnoreCase)
 82629                .ToList();
 630
 82631            var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.OrdinalIgnoreCase) && list.Any(i
 82632                .ToList();
 633
 164634            foreach (var dupe in dupes)
 635            {
 0636                _logger.LogInformation("Found duplicate path: {0}", dupe);
 637            }
 638
 82639            var newList = list.Except(dupes, StringComparer.OrdinalIgnoreCase).Select(_fileSystem.GetDirectoryInfo).ToLi
 82640            newList.AddRange(originalList.Where(i => !i.IsDirectory));
 82641            return newList;
 642        }
 643
 644        /// <summary>
 645        /// Determines whether a path should be ignored based on its contents - called after the contents have been read
 646        /// </summary>
 647        /// <param name="args">The args.</param>
 648        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
 649        private static bool ShouldResolvePathContents(ItemResolveArgs args)
 650        {
 651            // Ignore any folders containing a file called .ignore
 46652            return !args.ContainsFileSystemEntryByName(".ignore");
 653        }
 654
 655        public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryServ
 656        {
 61657            return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers);
 658        }
 659
 660        public IEnumerable<BaseItem> ResolvePaths(
 661            IEnumerable<FileSystemMetadata> files,
 662            IDirectoryService directoryService,
 663            Folder parent,
 664            LibraryOptions libraryOptions,
 665            CollectionType? collectionType,
 666            IItemResolver[] resolvers)
 667        {
 61668            var fileList = files.Where(i => !IgnoreFile(i, parent)).ToList();
 669
 61670            if (parent is not null)
 671            {
 61672                var multiItemResolvers = resolvers is null ? MultiItemResolvers : resolvers.OfType<IMultiItemResolver>()
 673
 366674                foreach (var resolver in multiItemResolvers)
 675                {
 122676                    var result = resolver.ResolveMultiple(parent, fileList, collectionType, directoryService);
 677
 122678                    if (result?.Items.Count > 0)
 679                    {
 0680                        var items = result.Items;
 0681                        items.RemoveAll(item => !ResolverHelper.SetInitialItemValues(item, parent, this, directoryServic
 0682                        items.AddRange(ResolveFileList(result.ExtraFiles, directoryService, parent, collectionType, reso
 0683                        return items;
 684                    }
 685                }
 686            }
 687
 61688            return ResolveFileList(fileList, directoryService, parent, collectionType, resolvers, libraryOptions);
 0689        }
 690
 691        private IEnumerable<BaseItem> ResolveFileList(
 692            IReadOnlyList<FileSystemMetadata> fileList,
 693            IDirectoryService directoryService,
 694            Folder? parent,
 695            CollectionType? collectionType,
 696            IItemResolver[]? resolvers,
 697            LibraryOptions libraryOptions)
 698        {
 699            // Given that fileList is a list we can save enumerator allocations by indexing
 700            for (var i = 0; i < fileList.Count; i++)
 701            {
 702                var file = fileList[i];
 703                BaseItem? result = null;
 704                try
 705                {
 706                    result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions);
 707                }
 708                catch (Exception ex)
 709                {
 710                    _logger.LogError(ex, "Error resolving path {Path}", file.FullName);
 711                }
 712
 713                if (result is not null)
 714                {
 715                    yield return result;
 716                }
 717            }
 718        }
 719
 720        /// <summary>
 721        /// Creates the root media folder.
 722        /// </summary>
 723        /// <returns>AggregateFolder.</returns>
 724        /// <exception cref="InvalidOperationException">Cannot create the root folder until plugins have loaded.</except
 725        public AggregateFolder CreateRootFolder()
 726        {
 22727            var rootFolderPath = _configurationManager.ApplicationPaths.RootFolderPath;
 728
 22729            Directory.CreateDirectory(rootFolderPath);
 730
 22731            var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ??
 22732                             (ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)) as Folder ?? throw new InvalidOp
 22733                             .DeepCopy<Folder, AggregateFolder>();
 734
 735            // In case program data folder was moved
 22736            if (!string.Equals(rootFolder.Path, rootFolderPath, StringComparison.Ordinal))
 737            {
 0738                _logger.LogInformation("Resetting root folder path to {0}", rootFolderPath);
 0739                rootFolder.Path = rootFolderPath;
 740            }
 741
 742            // Add in the plug-in folders
 22743            var path = Path.Combine(_configurationManager.ApplicationPaths.DataPath, "playlists");
 744
 22745            Directory.CreateDirectory(path);
 746
 22747            Folder folder = new PlaylistsFolder
 22748            {
 22749                Path = path
 22750            };
 751
 22752            if (folder.Id.IsEmpty())
 753            {
 22754                if (string.IsNullOrEmpty(folder.Path))
 755                {
 0756                    folder.Id = GetNewItemId(folder.GetType().Name, folder.GetType());
 757                }
 758                else
 759                {
 22760                    folder.Id = GetNewItemId(folder.Path, folder.GetType());
 761                }
 762            }
 763
 22764            var dbItem = GetItemById(folder.Id) as BasePluginFolder;
 765
 22766            if (dbItem is not null && string.Equals(dbItem.Path, folder.Path, StringComparison.OrdinalIgnoreCase))
 767            {
 0768                folder = dbItem;
 769            }
 770
 22771            if (!folder.ParentId.Equals(rootFolder.Id))
 772            {
 22773                folder.ParentId = rootFolder.Id;
 22774                folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetRe
 775            }
 776
 22777            rootFolder.AddVirtualChild(folder);
 778
 22779            RegisterItem(folder);
 780
 22781            return rootFolder;
 782        }
 783
 784        public Folder GetUserRootFolder()
 785        {
 790786            if (_userRootFolder is null)
 787            {
 22788                lock (_userRootFolderSyncLock)
 789                {
 22790                    if (_userRootFolder is null)
 791                    {
 22792                        var userRootPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 793
 22794                        _logger.LogDebug("Creating userRootPath at {Path}", userRootPath);
 22795                        Directory.CreateDirectory(userRootPath);
 796
 22797                        var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder));
 22798                        UserRootFolder? tmpItem = null;
 799                        try
 800                        {
 22801                            tmpItem = GetItemById(newItemId) as UserRootFolder;
 22802                        }
 0803                        catch (Exception ex)
 804                        {
 0805                            _logger.LogError(ex, "Error creating UserRootFolder {Path}", newItemId);
 0806                        }
 807
 22808                        if (tmpItem is null)
 809                        {
 22810                            _logger.LogDebug("Creating new userRootFolder with DeepCopy");
 22811                            tmpItem = (ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath)) as Folder ?? throw new In
 22812                                        .DeepCopy<Folder, UserRootFolder>();
 813                        }
 814
 815                        // In case program data folder was moved
 22816                        if (!string.Equals(tmpItem.Path, userRootPath, StringComparison.Ordinal))
 817                        {
 0818                            _logger.LogInformation("Resetting user root folder path to {0}", userRootPath);
 0819                            tmpItem.Path = userRootPath;
 820                        }
 821
 22822                        _userRootFolder = tmpItem;
 22823                        _logger.LogDebug("Setting userRootFolder: {Folder}", _userRootFolder);
 824                    }
 22825                }
 826            }
 827
 790828            return _userRootFolder;
 829        }
 830
 831        /// <inheritdoc />
 832        public BaseItem? FindByPath(string path, bool? isFolder)
 833        {
 834            // If this returns multiple items it could be tricky figuring out which one is correct.
 835            // In most cases, the newest one will be and the others obsolete but not yet cleaned up
 0836            ArgumentException.ThrowIfNullOrEmpty(path);
 837
 0838            var query = new InternalItemsQuery
 0839            {
 0840                Path = path,
 0841                IsFolder = isFolder,
 0842                OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) },
 0843                Limit = 1,
 0844                DtoOptions = new DtoOptions(true)
 0845            };
 846
 0847            return GetItemList(query)
 0848                .FirstOrDefault();
 849        }
 850
 851        /// <inheritdoc />
 852        public Person? GetPerson(string name)
 853        {
 1854            var path = Person.GetPath(name);
 1855            var id = GetItemByNameId<Person>(path);
 1856            if (GetItemById(id) is Person item)
 857            {
 0858                return item;
 859            }
 860
 1861            return null;
 862        }
 863
 864        /// <summary>
 865        /// Gets the studio.
 866        /// </summary>
 867        /// <param name="name">The name.</param>
 868        /// <returns>Task{Studio}.</returns>
 869        public Studio GetStudio(string name)
 870        {
 0871            return CreateItemByName<Studio>(Studio.GetPath, name, new DtoOptions(true));
 872        }
 873
 874        public Guid GetStudioId(string name)
 875        {
 0876            return GetItemByNameId<Studio>(Studio.GetPath(name));
 877        }
 878
 879        public Guid GetGenreId(string name)
 880        {
 0881            return GetItemByNameId<Genre>(Genre.GetPath(name));
 882        }
 883
 884        public Guid GetMusicGenreId(string name)
 885        {
 0886            return GetItemByNameId<MusicGenre>(MusicGenre.GetPath(name));
 887        }
 888
 889        /// <summary>
 890        /// Gets the genre.
 891        /// </summary>
 892        /// <param name="name">The name.</param>
 893        /// <returns>Task{Genre}.</returns>
 894        public Genre GetGenre(string name)
 895        {
 0896            return CreateItemByName<Genre>(Genre.GetPath, name, new DtoOptions(true));
 897        }
 898
 899        /// <summary>
 900        /// Gets the music genre.
 901        /// </summary>
 902        /// <param name="name">The name.</param>
 903        /// <returns>Task{MusicGenre}.</returns>
 904        public MusicGenre GetMusicGenre(string name)
 905        {
 0906            return CreateItemByName<MusicGenre>(MusicGenre.GetPath, name, new DtoOptions(true));
 907        }
 908
 909        /// <summary>
 910        /// Gets the year.
 911        /// </summary>
 912        /// <param name="value">The value.</param>
 913        /// <returns>Task{Year}.</returns>
 914        public Year GetYear(int value)
 915        {
 0916            if (value <= 0)
 917            {
 0918                throw new ArgumentOutOfRangeException(nameof(value), "Years less than or equal to 0 are invalid.");
 919            }
 920
 0921            var name = value.ToString(CultureInfo.InvariantCulture);
 922
 0923            return CreateItemByName<Year>(Year.GetPath, name, new DtoOptions(true));
 924        }
 925
 926        /// <summary>
 927        /// Gets a Genre.
 928        /// </summary>
 929        /// <param name="name">The name.</param>
 930        /// <returns>Task{Genre}.</returns>
 931        public MusicArtist GetArtist(string name)
 932        {
 0933            return GetArtist(name, new DtoOptions(true));
 934        }
 935
 936        public MusicArtist GetArtist(string name, DtoOptions options)
 937        {
 0938            return CreateItemByName<MusicArtist>(MusicArtist.GetPath, name, options);
 939        }
 940
 941        private T CreateItemByName<T>(Func<string, string> getPathFn, string name, DtoOptions options)
 942            where T : BaseItem, new()
 943        {
 0944            if (typeof(T) == typeof(MusicArtist))
 945            {
 0946                var existing = GetItemList(new InternalItemsQuery
 0947                {
 0948                    IncludeItemTypes = new[] { BaseItemKind.MusicArtist },
 0949                    Name = name,
 0950                    DtoOptions = options
 0951                }).Cast<MusicArtist>()
 0952                .OrderBy(i => i.IsAccessedByName ? 1 : 0)
 0953                .Cast<T>()
 0954                .FirstOrDefault();
 955
 0956                if (existing is not null)
 957                {
 0958                    return existing;
 959                }
 960            }
 961
 0962            var path = getPathFn(name);
 0963            var id = GetItemByNameId<T>(path);
 0964            var item = GetItemById(id) as T;
 0965            if (item is null)
 966            {
 0967                item = new T
 0968                {
 0969                    Name = name,
 0970                    Id = id,
 0971                    DateCreated = DateTime.UtcNow,
 0972                    DateModified = DateTime.UtcNow,
 0973                    Path = path
 0974                };
 975
 0976                CreateItem(item, null);
 977            }
 978
 0979            return item;
 980        }
 981
 982        private Guid GetItemByNameId<T>(string path)
 983              where T : BaseItem, new()
 984        {
 1985            var forceCaseInsensitiveId = _configurationManager.Configuration.EnableNormalizedItemByNameIds;
 1986            return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId);
 987        }
 988
 989        /// <inheritdoc />
 990        public Task ValidatePeopleAsync(IProgress<double> progress, CancellationToken cancellationToken)
 991        {
 992            // Ensure the location is available.
 0993            Directory.CreateDirectory(_configurationManager.ApplicationPaths.PeoplePath);
 994
 0995            return new PeopleValidator(this, _logger, _fileSystem).ValidatePeople(cancellationToken, progress);
 996        }
 997
 998        /// <summary>
 999        /// Reloads the root media folder.
 1000        /// </summary>
 1001        /// <param name="progress">The progress.</param>
 1002        /// <param name="cancellationToken">The cancellation token.</param>
 1003        /// <returns>Task.</returns>
 1004        public Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken)
 1005        {
 1006            // Just run the scheduled task so that the user can see it
 21007            _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
 1008
 21009            return Task.CompletedTask;
 1010        }
 1011
 1012        /// <summary>
 1013        /// Validates the media library internal.
 1014        /// </summary>
 1015        /// <param name="progress">The progress.</param>
 1016        /// <param name="cancellationToken">The cancellation token.</param>
 1017        /// <returns>Task.</returns>
 1018        public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken)
 1019        {
 1020            IsScanRunning = true;
 1021            LibraryMonitor.Stop();
 1022
 1023            try
 1024            {
 1025                await PerformLibraryValidation(progress, cancellationToken).ConfigureAwait(false);
 1026            }
 1027            finally
 1028            {
 1029                LibraryMonitor.Start();
 1030                IsScanRunning = false;
 1031            }
 1032        }
 1033
 1034        public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
 1035        {
 1036            await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1037
 1038            // Start by just validating the children of the root, but go no further
 1039            await RootFolder.ValidateChildren(
 1040                new Progress<double>(),
 1041                new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
 1042                recursive: false,
 1043                allowRemoveRoot: removeRoot,
 1044                cancellationToken: cancellationToken).ConfigureAwait(false);
 1045
 1046            await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1047
 1048            await GetUserRootFolder().ValidateChildren(
 1049                new Progress<double>(),
 1050                new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
 1051                recursive: false,
 1052                allowRemoveRoot: removeRoot,
 1053                cancellationToken: cancellationToken).ConfigureAwait(false);
 1054
 1055            // Quickly scan CollectionFolders for changes
 1056            foreach (var folder in GetUserRootFolder().Children.OfType<Folder>())
 1057            {
 1058                await folder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1059            }
 1060        }
 1061
 1062        private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken)
 1063        {
 1064            _logger.LogInformation("Validating media library");
 1065
 1066            await ValidateTopLibraryFolders(cancellationToken).ConfigureAwait(false);
 1067
 1068            var innerProgress = new Progress<double>(pct => progress.Report(pct * 0.96));
 1069
 1070            // Validate the entire media library
 1071            await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem
 1072
 1073            progress.Report(96);
 1074
 1075            innerProgress = new Progress<double>(pct => progress.Report(96 + (pct * .04)));
 1076
 1077            await RunPostScanTasks(innerProgress, cancellationToken).ConfigureAwait(false);
 1078
 1079            progress.Report(100);
 1080        }
 1081
 1082        /// <summary>
 1083        /// Runs the post scan tasks.
 1084        /// </summary>
 1085        /// <param name="progress">The progress.</param>
 1086        /// <param name="cancellationToken">The cancellation token.</param>
 1087        /// <returns>Task.</returns>
 1088        private async Task RunPostScanTasks(IProgress<double> progress, CancellationToken cancellationToken)
 1089        {
 1090            var tasks = PostscanTasks.ToList();
 1091
 1092            var numComplete = 0;
 1093            var numTasks = tasks.Count;
 1094
 1095            foreach (var task in tasks)
 1096            {
 1097                // Prevent access to modified closure
 1098                var currentNumComplete = numComplete;
 1099
 1100                var innerProgress = new Progress<double>(pct =>
 1101                {
 1102                    double innerPercent = pct;
 1103                    innerPercent /= 100;
 1104                    innerPercent += currentNumComplete;
 1105
 1106                    innerPercent /= numTasks;
 1107                    innerPercent *= 100;
 1108
 1109                    progress.Report(innerPercent);
 1110                });
 1111
 1112                _logger.LogDebug("Running post-scan task {0}", task.GetType().Name);
 1113
 1114                try
 1115                {
 1116                    await task.Run(innerProgress, cancellationToken).ConfigureAwait(false);
 1117                }
 1118                catch (OperationCanceledException)
 1119                {
 1120                    _logger.LogInformation("Post-scan task cancelled: {0}", task.GetType().Name);
 1121                    throw;
 1122                }
 1123                catch (Exception ex)
 1124                {
 1125                    _logger.LogError(ex, "Error running post-scan task");
 1126                }
 1127
 1128                numComplete++;
 1129                double percent = numComplete;
 1130                percent /= numTasks;
 1131                progress.Report(percent * 100);
 1132            }
 1133
 1134            _itemRepository.UpdateInheritedValues();
 1135
 1136            progress.Report(100);
 1137        }
 1138
 1139        /// <summary>
 1140        /// Gets the default view.
 1141        /// </summary>
 1142        /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
 1143        public List<VirtualFolderInfo> GetVirtualFolders()
 1144        {
 241145            return GetVirtualFolders(false);
 1146        }
 1147
 1148        public List<VirtualFolderInfo> GetVirtualFolders(bool includeRefreshState)
 1149        {
 251150            _logger.LogDebug("Getting topLibraryFolders");
 251151            var topLibraryFolders = GetUserRootFolder().Children.ToList();
 1152
 251153            _logger.LogDebug("Getting refreshQueue");
 251154            var refreshQueue = includeRefreshState ? ProviderManager.GetRefreshQueue() : null;
 1155
 251156            return _fileSystem.GetDirectoryPaths(_configurationManager.ApplicationPaths.DefaultUserViewsPath)
 251157                .Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders, refreshQueue))
 251158                .ToList();
 1159        }
 1160
 1161        private VirtualFolderInfo GetVirtualFolderInfo(string dir, List<BaseItem> allCollectionFolders, HashSet<Guid>? r
 1162        {
 11163            var info = new VirtualFolderInfo
 11164            {
 11165                Name = Path.GetFileName(dir),
 11166
 11167                Locations = _fileSystem.GetFilePaths(dir, false)
 11168                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCa
 11169                    .Select(i =>
 11170                    {
 11171                        try
 11172                        {
 11173                            return _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(i));
 11174                        }
 11175                        catch (Exception ex)
 11176                        {
 11177                            _logger.LogError(ex, "Error resolving shortcut file {File}", i);
 11178                            return null;
 11179                        }
 11180                    })
 11181                    .Where(i => i is not null)
 11182                    .Order()
 11183                    .ToArray(),
 11184
 11185                CollectionType = GetCollectionType(dir)
 11186            };
 1187
 11188            var libraryFolder = allCollectionFolders.FirstOrDefault(i => string.Equals(i.Path, dir, StringComparison.Ord
 11189            if (libraryFolder is not null)
 1190            {
 11191                var libraryFolderId = libraryFolder.Id.ToString("N", CultureInfo.InvariantCulture);
 11192                info.ItemId = libraryFolderId;
 11193                if (libraryFolder.HasImage(ImageType.Primary))
 1194                {
 01195                    info.PrimaryImageItemId = libraryFolderId;
 1196                }
 1197
 11198                info.LibraryOptions = GetLibraryOptions(libraryFolder);
 1199
 11200                if (refreshQueue is not null)
 1201                {
 11202                    info.RefreshProgress = libraryFolder.GetRefreshProgress();
 1203
 11204                    info.RefreshStatus = info.RefreshProgress.HasValue ? "Active" : refreshQueue.Contains(libraryFolder.
 1205                }
 1206            }
 1207
 11208            return info;
 1209        }
 1210
 1211        private CollectionTypeOptions? GetCollectionType(string path)
 1212        {
 11213            var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false);
 21214            foreach (ReadOnlySpan<char> file in files)
 1215            {
 01216                if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
 1217                {
 01218                    return res;
 1219                }
 1220            }
 1221
 11222            return null;
 01223        }
 1224
 1225        /// <inheritdoc />
 1226        public BaseItem? GetItemById(Guid id)
 1227        {
 5251228            if (id.IsEmpty())
 1229            {
 01230                throw new ArgumentException("Guid can't be empty", nameof(id));
 1231            }
 1232
 5251233            if (_cache.TryGetValue(id, out BaseItem? item))
 1234            {
 2681235                return item;
 1236            }
 1237
 2571238            item = RetrieveItem(id);
 1239
 2571240            if (item is not null)
 1241            {
 01242                RegisterItem(item);
 1243            }
 1244
 2571245            return item;
 1246        }
 1247
 1248        /// <inheritdoc />
 1249        public T? GetItemById<T>(Guid id)
 1250         where T : BaseItem
 1251        {
 271252            var item = GetItemById(id);
 271253            if (item is T typedItem)
 1254            {
 51255                return typedItem;
 1256            }
 1257
 221258            return null;
 1259        }
 1260
 1261        /// <inheritdoc />
 1262        public T? GetItemById<T>(Guid id, Guid userId)
 1263            where T : BaseItem
 1264        {
 11265            var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
 11266            return GetItemById<T>(id, user);
 1267        }
 1268
 1269        /// <inheritdoc />
 1270        public T? GetItemById<T>(Guid id, User? user)
 1271            where T : BaseItem
 1272        {
 251273            var item = GetItemById<T>(id);
 251274            return ItemIsVisible(item, user) ? item : null;
 1275        }
 1276
 1277        public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
 1278        {
 841279            if (query.Recursive && !query.ParentId.IsEmpty())
 1280            {
 431281                var parent = GetItemById(query.ParentId);
 431282                if (parent is not null)
 1283                {
 261284                    SetTopParentIdsOrAncestors(query, new[] { parent });
 1285                }
 1286            }
 1287
 841288            if (query.User is not null)
 1289            {
 11290                AddUserToQuery(query, query.User, allowExternalContent);
 1291            }
 1292
 841293            var itemList = _itemRepository.GetItemList(query);
 841294            var user = query.User;
 841295            if (user is not null)
 1296            {
 11297                return itemList.Where(i => i.IsVisible(user)).ToList();
 1298            }
 1299
 831300            return itemList;
 1301        }
 1302
 1303        public List<BaseItem> GetItemList(InternalItemsQuery query)
 1304        {
 841305            return GetItemList(query, true);
 1306        }
 1307
 1308        public int GetCount(InternalItemsQuery query)
 1309        {
 01310            if (query.Recursive && !query.ParentId.IsEmpty())
 1311            {
 01312                var parent = GetItemById(query.ParentId);
 01313                if (parent is not null)
 1314                {
 01315                    SetTopParentIdsOrAncestors(query, new[] { parent });
 1316                }
 1317            }
 1318
 01319            if (query.User is not null)
 1320            {
 01321                AddUserToQuery(query, query.User);
 1322            }
 1323
 01324            return _itemRepository.GetCount(query);
 1325        }
 1326
 1327        public List<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
 1328        {
 01329            SetTopParentIdsOrAncestors(query, parents);
 1330
 01331            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1332            {
 01333                if (query.User is not null)
 1334                {
 01335                    AddUserToQuery(query, query.User);
 1336                }
 1337            }
 1338
 01339            return _itemRepository.GetItemList(query);
 1340        }
 1341
 1342        public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
 1343        {
 01344            if (query.User is not null)
 1345            {
 01346                AddUserToQuery(query, query.User);
 1347            }
 1348
 01349            if (query.EnableTotalRecordCount)
 1350            {
 01351                return _itemRepository.GetItems(query);
 1352            }
 1353
 01354            return new QueryResult<BaseItem>(
 01355                query.StartIndex,
 01356                null,
 01357                _itemRepository.GetItemList(query));
 1358        }
 1359
 1360        public List<Guid> GetItemIds(InternalItemsQuery query)
 1361        {
 191362            if (query.User is not null)
 1363            {
 01364                AddUserToQuery(query, query.User);
 1365            }
 1366
 191367            return _itemRepository.GetItemIdsList(query);
 1368        }
 1369
 1370        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query)
 1371        {
 01372            if (query.User is not null)
 1373            {
 01374                AddUserToQuery(query, query.User);
 1375            }
 1376
 01377            SetTopParentOrAncestorIds(query);
 01378            return _itemRepository.GetStudios(query);
 1379        }
 1380
 1381        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query)
 1382        {
 01383            if (query.User is not null)
 1384            {
 01385                AddUserToQuery(query, query.User);
 1386            }
 1387
 01388            SetTopParentOrAncestorIds(query);
 01389            return _itemRepository.GetGenres(query);
 1390        }
 1391
 1392        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query)
 1393        {
 01394            if (query.User is not null)
 1395            {
 01396                AddUserToQuery(query, query.User);
 1397            }
 1398
 01399            SetTopParentOrAncestorIds(query);
 01400            return _itemRepository.GetMusicGenres(query);
 1401        }
 1402
 1403        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query)
 1404        {
 01405            if (query.User is not null)
 1406            {
 01407                AddUserToQuery(query, query.User);
 1408            }
 1409
 01410            SetTopParentOrAncestorIds(query);
 01411            return _itemRepository.GetAllArtists(query);
 1412        }
 1413
 1414        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query)
 1415        {
 01416            if (query.User is not null)
 1417            {
 01418                AddUserToQuery(query, query.User);
 1419            }
 1420
 01421            SetTopParentOrAncestorIds(query);
 01422            return _itemRepository.GetArtists(query);
 1423        }
 1424
 1425        private void SetTopParentOrAncestorIds(InternalItemsQuery query)
 1426        {
 01427            var ancestorIds = query.AncestorIds;
 01428            int len = ancestorIds.Length;
 01429            if (len == 0)
 1430            {
 01431                return;
 1432            }
 1433
 01434            var parents = new BaseItem[len];
 01435            for (int i = 0; i < len; i++)
 1436            {
 01437                parents[i] = GetItemById(ancestorIds[i]) ?? throw new ArgumentException($"Failed to find parent with id:
 01438                if (parents[i] is not (ICollectionFolder or UserView))
 1439                {
 01440                    return;
 1441                }
 1442            }
 1443
 1444            // Optimize by querying against top level views
 01445            query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
 01446            query.AncestorIds = Array.Empty<Guid>();
 1447
 1448            // Prevent searching in all libraries due to empty filter
 01449            if (query.TopParentIds.Length == 0)
 1450            {
 01451                query.TopParentIds = [Guid.NewGuid()];
 1452            }
 01453        }
 1454
 1455        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
 1456        {
 01457            if (query.User is not null)
 1458            {
 01459                AddUserToQuery(query, query.User);
 1460            }
 1461
 01462            SetTopParentOrAncestorIds(query);
 01463            return _itemRepository.GetAlbumArtists(query);
 1464        }
 1465
 1466        public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query)
 1467        {
 51468            if (query.Recursive && !query.ParentId.IsEmpty())
 1469            {
 41470                var parent = GetItemById(query.ParentId);
 41471                if (parent is not null)
 1472                {
 41473                    SetTopParentIdsOrAncestors(query, new[] { parent });
 1474                }
 1475            }
 1476
 51477            if (query.User is not null)
 1478            {
 11479                AddUserToQuery(query, query.User);
 1480            }
 1481
 51482            if (query.EnableTotalRecordCount)
 1483            {
 11484                return _itemRepository.GetItems(query);
 1485            }
 1486
 41487            return new QueryResult<BaseItem>(
 41488                query.StartIndex,
 41489                null,
 41490                _itemRepository.GetItemList(query));
 1491        }
 1492
 1493        private void SetTopParentIdsOrAncestors(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents)
 1494        {
 301495            if (parents.All(i => i is ICollectionFolder || i is UserView))
 1496            {
 1497                // Optimize by querying against top level views
 41498                query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
 1499
 1500                // Prevent searching in all libraries due to empty filter
 41501                if (query.TopParentIds.Length == 0)
 1502                {
 41503                    query.TopParentIds = new[] { Guid.NewGuid() };
 1504                }
 1505            }
 1506            else
 1507            {
 1508                // We need to be able to query from any arbitrary ancestor up the tree
 261509                query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).ToArray();
 1510
 1511                // Prevent searching in all libraries due to empty filter
 261512                if (query.AncestorIds.Length == 0)
 1513                {
 01514                    query.AncestorIds = new[] { Guid.NewGuid() };
 1515                }
 1516            }
 1517
 301518            query.Parent = null;
 301519        }
 1520
 1521        private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
 1522        {
 21523            if (query.AncestorIds.Length == 0 &&
 21524                query.ParentId.IsEmpty() &&
 21525                query.ChannelIds.Count == 0 &&
 21526                query.TopParentIds.Length == 0 &&
 21527                string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) &&
 21528                string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) &&
 21529                query.ItemIds.Length == 0)
 1530            {
 11531                var userViews = UserViewManager.GetUserViews(new UserViewQuery
 11532                {
 11533                    User = user,
 11534                    IncludeHidden = true,
 11535                    IncludeExternalContent = allowExternalContent
 11536                });
 1537
 11538                query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).ToArray();
 1539
 1540                // Prevent searching in all libraries due to empty filter
 11541                if (query.TopParentIds.Length == 0)
 1542                {
 11543                    query.TopParentIds = new[] { Guid.NewGuid() };
 1544                }
 1545            }
 21546        }
 1547
 1548        private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User? user)
 1549        {
 41550            if (item is UserView view)
 1551            {
 01552                if (view.ViewType == CollectionType.livetv)
 1553                {
 01554                    return new[] { view.Id };
 1555                }
 1556
 1557                // Translate view into folders
 01558                if (!view.DisplayParentId.IsEmpty())
 1559                {
 01560                    var displayParent = GetItemById(view.DisplayParentId);
 01561                    if (displayParent is not null)
 1562                    {
 01563                        return GetTopParentIdsForQuery(displayParent, user);
 1564                    }
 1565
 01566                    return Array.Empty<Guid>();
 1567                }
 1568
 01569                if (!view.ParentId.IsEmpty())
 1570                {
 01571                    var displayParent = GetItemById(view.ParentId);
 01572                    if (displayParent is not null)
 1573                    {
 01574                        return GetTopParentIdsForQuery(displayParent, user);
 1575                    }
 1576
 01577                    return Array.Empty<Guid>();
 1578                }
 1579
 1580                // Handle grouping
 01581                if (user is not null && view.ViewType != CollectionType.unknown && UserView.IsEligibleForGrouping(view.V
 01582                    && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0)
 1583                {
 01584                    return GetUserRootFolder()
 01585                        .GetChildren(user, true)
 01586                        .OfType<CollectionFolder>()
 01587                        .Where(i => i.CollectionType is null || i.CollectionType == view.ViewType)
 01588                        .Where(i => user.IsFolderGrouped(i.Id))
 01589                        .SelectMany(i => GetTopParentIdsForQuery(i, user));
 1590                }
 1591
 01592                return Array.Empty<Guid>();
 1593            }
 1594
 41595            if (item is CollectionFolder collectionFolder)
 1596            {
 41597                return collectionFolder.PhysicalFolderIds;
 1598            }
 1599
 01600            var topParent = item.GetTopParent();
 01601            if (topParent is not null)
 1602            {
 01603                return new[] { topParent.Id };
 1604            }
 1605
 01606            return Array.Empty<Guid>();
 1607        }
 1608
 1609        /// <summary>
 1610        /// Gets the intros.
 1611        /// </summary>
 1612        /// <param name="item">The item.</param>
 1613        /// <param name="user">The user.</param>
 1614        /// <returns>IEnumerable{System.String}.</returns>
 1615        public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
 1616        {
 1617            if (IntroProviders.Length == 0)
 1618            {
 1619                return [];
 1620            }
 1621
 1622            var tasks = IntroProviders
 1623                .Select(i => GetIntros(i, item, user));
 1624
 1625            var items = await Task.WhenAll(tasks).ConfigureAwait(false);
 1626
 1627            return items
 1628                .SelectMany(i => i)
 1629                .Select(ResolveIntro)
 1630                .Where(i => i is not null)!; // null values got filtered out
 1631        }
 1632
 1633        /// <summary>
 1634        /// Gets the intros.
 1635        /// </summary>
 1636        /// <param name="provider">The provider.</param>
 1637        /// <param name="item">The item.</param>
 1638        /// <param name="user">The user.</param>
 1639        /// <returns>Task&lt;IEnumerable&lt;IntroInfo&gt;&gt;.</returns>
 1640        private async Task<IEnumerable<IntroInfo>> GetIntros(IIntroProvider provider, BaseItem item, User user)
 1641        {
 1642            try
 1643            {
 1644                return await provider.GetIntros(item, user).ConfigureAwait(false);
 1645            }
 1646            catch (Exception ex)
 1647            {
 1648                _logger.LogError(ex, "Error getting intros");
 1649
 1650                return Enumerable.Empty<IntroInfo>();
 1651            }
 1652        }
 1653
 1654        /// <summary>
 1655        /// Resolves the intro.
 1656        /// </summary>
 1657        /// <param name="info">The info.</param>
 1658        /// <returns>Video.</returns>
 1659        private Video? ResolveIntro(IntroInfo info)
 1660        {
 01661            Video? video = null;
 1662
 01663            if (info.ItemId.HasValue)
 1664            {
 1665                // Get an existing item by Id
 01666                video = GetItemById(info.ItemId.Value) as Video;
 1667
 01668                if (video is null)
 1669                {
 01670                    _logger.LogError("Unable to locate item with Id {ID}.", info.ItemId.Value);
 1671                }
 1672            }
 01673            else if (!string.IsNullOrEmpty(info.Path))
 1674            {
 1675                try
 1676                {
 1677                    // Try to resolve the path into a video
 01678                    video = ResolvePath(_fileSystem.GetFileSystemInfo(info.Path)) as Video;
 1679
 01680                    if (video is null)
 1681                    {
 01682                        _logger.LogError("Intro resolver returned null for {Path}.", info.Path);
 1683                    }
 1684                    else
 1685                    {
 1686                        // Pull the saved db item that will include metadata
 01687                        var dbItem = GetItemById(video.Id) as Video;
 1688
 01689                        if (dbItem is not null)
 1690                        {
 01691                            video = dbItem;
 1692                        }
 1693                        else
 1694                        {
 01695                            return null;
 1696                        }
 1697                    }
 01698                }
 01699                catch (Exception ex)
 1700                {
 01701                    _logger.LogError(ex, "Error resolving path {Path}.", info.Path);
 01702                }
 1703            }
 1704            else
 1705            {
 01706                _logger.LogError("IntroProvider returned an IntroInfo with null Path and ItemId.");
 1707            }
 1708
 01709            return video;
 01710        }
 1711
 1712        /// <inheritdoc />
 1713        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortO
 1714        {
 11715            IOrderedEnumerable<BaseItem>? orderedItems = null;
 1716
 41717            foreach (var orderBy in sortBy.Select(o => GetComparer(o, user)).Where(c => c is not null))
 1718            {
 11719                if (orderBy is RandomComparer)
 1720                {
 01721                    var randomItems = items.ToArray();
 01722                    Random.Shared.Shuffle(randomItems);
 01723                    items = randomItems;
 1724                    // Items are no longer ordered at this point, so set orderedItems back to null
 01725                    orderedItems = null;
 1726                }
 11727                else if (orderedItems is null)
 1728                {
 11729                    orderedItems = sortOrder == SortOrder.Descending
 11730                        ? items.OrderByDescending(i => i, orderBy)
 11731                        : items.OrderBy(i => i, orderBy);
 1732                }
 1733                else
 1734                {
 01735                    orderedItems = sortOrder == SortOrder.Descending
 01736                        ? orderedItems!.ThenByDescending(i => i, orderBy)
 01737                        : orderedItems!.ThenBy(i => i, orderBy); // orderedItems is set during the first iteration
 1738                }
 1739            }
 1740
 11741            return orderedItems ?? items;
 1742        }
 1743
 1744        /// <inheritdoc />
 1745        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<(ItemSortBy OrderBy, Sort
 1746        {
 01747            IOrderedEnumerable<BaseItem>? orderedItems = null;
 1748
 01749            foreach (var (name, sortOrder) in orderBy)
 1750            {
 01751                var comparer = GetComparer(name, user);
 01752                if (comparer is null)
 1753                {
 1754                    continue;
 1755                }
 1756
 01757                if (comparer is RandomComparer)
 1758                {
 01759                    var randomItems = items.ToArray();
 01760                    Random.Shared.Shuffle(randomItems);
 01761                    items = randomItems;
 1762                    // Items are no longer ordered at this point, so set orderedItems back to null
 01763                    orderedItems = null;
 1764                }
 01765                else if (orderedItems is null)
 1766                {
 01767                    orderedItems = sortOrder == SortOrder.Descending
 01768                        ? items.OrderByDescending(i => i, comparer)
 01769                        : items.OrderBy(i => i, comparer);
 1770                }
 1771                else
 1772                {
 01773                    orderedItems = sortOrder == SortOrder.Descending
 01774                        ? orderedItems!.ThenByDescending(i => i, comparer)
 01775                        : orderedItems!.ThenBy(i => i, comparer); // orderedItems is set during the first iteration
 1776                }
 1777            }
 1778
 01779            return orderedItems ?? items;
 1780        }
 1781
 1782        /// <summary>
 1783        /// Gets the comparer.
 1784        /// </summary>
 1785        /// <param name="name">The name.</param>
 1786        /// <param name="user">The user.</param>
 1787        /// <returns>IBaseItemComparer.</returns>
 1788        private IBaseItemComparer? GetComparer(ItemSortBy name, User? user)
 1789        {
 11790            var comparer = Comparers.FirstOrDefault(c => name == c.Type);
 1791
 1792            // If it requires a user, create a new one, and assign the user
 11793            if (comparer is IUserBaseItemComparer)
 1794            {
 01795                var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType())!; // only null fo
 1796
 01797                userComparer.User = user;
 01798                userComparer.UserManager = _userManager;
 01799                userComparer.UserDataRepository = _userDataRepository;
 1800
 01801                return userComparer;
 1802            }
 1803
 11804            return comparer;
 1805        }
 1806
 1807        /// <inheritdoc />
 1808        public void CreateItem(BaseItem item, BaseItem? parent)
 1809        {
 01810            CreateItems(new[] { item }, parent, CancellationToken.None);
 01811        }
 1812
 1813        /// <inheritdoc />
 1814        public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
 1815        {
 11816            _itemRepository.SaveItems(items, cancellationToken);
 1817
 41818            foreach (var item in items)
 1819            {
 11820                RegisterItem(item);
 1821            }
 1822
 11823            if (ItemAdded is not null)
 1824            {
 41825                foreach (var item in items)
 1826                {
 1827                    // With the live tv guide this just creates too much noise
 11828                    if (item.SourceType != SourceType.Library)
 1829                    {
 1830                        continue;
 1831                    }
 1832
 1833                    try
 1834                    {
 11835                        ItemAdded(
 11836                            this,
 11837                            new ItemChangeEventArgs
 11838                            {
 11839                                Item = item,
 11840                                Parent = parent ?? item.GetParent()
 11841                            });
 11842                    }
 01843                    catch (Exception ex)
 1844                    {
 01845                        _logger.LogError(ex, "Error in ItemAdded event handler");
 01846                    }
 1847                }
 1848            }
 11849        }
 1850
 1851        private bool ImageNeedsRefresh(ItemImageInfo image)
 1852        {
 01853            if (image.Path is not null && image.IsLocalFile)
 1854            {
 01855                if (image.Width == 0 || image.Height == 0 || string.IsNullOrEmpty(image.BlurHash))
 1856                {
 01857                    return true;
 1858                }
 1859
 1860                try
 1861                {
 01862                    return _fileSystem.GetLastWriteTimeUtc(image.Path) != image.DateModified;
 1863                }
 01864                catch (Exception ex)
 1865                {
 01866                    _logger.LogError(ex, "Cannot get file info for {0}", image.Path);
 01867                    return false;
 1868                }
 1869            }
 1870
 01871            return image.Path is not null && !image.IsLocalFile;
 01872        }
 1873
 1874        /// <inheritdoc />
 1875        public async Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false)
 1876        {
 1877            ArgumentNullException.ThrowIfNull(item);
 1878
 1879            var outdated = forceUpdate
 1880                ? item.ImageInfos.Where(i => i.Path is not null).ToArray()
 1881                : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
 1882            // Skip image processing if current or live tv source
 1883            if (outdated.Length == 0 || item.SourceType != SourceType.Library)
 1884            {
 1885                RegisterItem(item);
 1886                return;
 1887            }
 1888
 1889            foreach (var img in outdated)
 1890            {
 1891                var image = img;
 1892                if (!img.IsLocalFile)
 1893                {
 1894                    try
 1895                    {
 1896                        var index = item.GetImageIndex(img);
 1897                        image = await ConvertImageToLocal(item, img, index, true).ConfigureAwait(false);
 1898                    }
 1899                    catch (ArgumentException)
 1900                    {
 1901                        _logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
 1902                        continue;
 1903                    }
 1904                    catch (Exception ex) when (ex is InvalidOperationException or IOException)
 1905                    {
 1906                        _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path);
 1907                        continue;
 1908                    }
 1909                    catch (HttpRequestException ex)
 1910                    {
 1911                        _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}. Http status code: {HttpStatus}", im
 1912                        continue;
 1913                    }
 1914                }
 1915
 1916                ImageDimensions size;
 1917                try
 1918                {
 1919                    size = _imageProcessor.GetImageDimensions(item, image);
 1920                    image.Width = size.Width;
 1921                    image.Height = size.Height;
 1922                }
 1923                catch (Exception ex)
 1924                {
 1925                    _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
 1926                    size = default;
 1927                    image.Width = 0;
 1928                    image.Height = 0;
 1929                }
 1930
 1931                try
 1932                {
 1933                    image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path, size);
 1934                }
 1935                catch (Exception ex)
 1936                {
 1937                    _logger.LogError(ex, "Cannot compute blurhash for {ImagePath}", image.Path);
 1938                    image.BlurHash = string.Empty;
 1939                }
 1940
 1941                try
 1942                {
 1943                    image.DateModified = _fileSystem.GetLastWriteTimeUtc(image.Path);
 1944                }
 1945                catch (Exception ex)
 1946                {
 1947                    _logger.LogError(ex, "Cannot update DateModified for {ImagePath}", image.Path);
 1948                }
 1949            }
 1950
 1951            _itemRepository.SaveImages(item);
 1952            RegisterItem(item);
 1953        }
 1954
 1955        /// <inheritdoc />
 1956        public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, 
 1957        {
 1958            foreach (var item in items)
 1959            {
 1960                await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
 1961            }
 1962
 1963            _itemRepository.SaveItems(items, cancellationToken);
 1964
 1965            if (ItemUpdated is not null)
 1966            {
 1967                foreach (var item in items)
 1968                {
 1969                    // With the live tv guide this just creates too much noise
 1970                    if (item.SourceType != SourceType.Library)
 1971                    {
 1972                        continue;
 1973                    }
 1974
 1975                    try
 1976                    {
 1977                        ItemUpdated(
 1978                            this,
 1979                            new ItemChangeEventArgs
 1980                            {
 1981                                Item = item,
 1982                                Parent = parent,
 1983                                UpdateReason = updateReason
 1984                            });
 1985                    }
 1986                    catch (Exception ex)
 1987                    {
 1988                        _logger.LogError(ex, "Error in ItemUpdated event handler");
 1989                    }
 1990                }
 1991            }
 1992        }
 1993
 1994        /// <inheritdoc />
 1995        public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cance
 581996            => UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken);
 1997
 1998        public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
 1999        {
 2000            if (item.IsFileProtocol)
 2001            {
 2002                await ProviderManager.SaveMetadataAsync(item, updateReason).ConfigureAwait(false);
 2003            }
 2004
 2005            item.DateLastSaved = DateTime.UtcNow;
 2006
 2007            await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
 2008        }
 2009
 2010        /// <summary>
 2011        /// Reports the item removed.
 2012        /// </summary>
 2013        /// <param name="item">The item.</param>
 2014        /// <param name="parent">The parent item.</param>
 2015        public void ReportItemRemoved(BaseItem item, BaseItem parent)
 2016        {
 22017            if (ItemRemoved is not null)
 2018            {
 2019                try
 2020                {
 22021                    ItemRemoved(
 22022                        this,
 22023                        new ItemChangeEventArgs
 22024                        {
 22025                            Item = item,
 22026                            Parent = parent
 22027                        });
 22028                }
 02029                catch (Exception ex)
 2030                {
 02031                    _logger.LogError(ex, "Error in ItemRemoved event handler");
 02032                }
 2033            }
 22034        }
 2035
 2036        /// <summary>
 2037        /// Retrieves the item.
 2038        /// </summary>
 2039        /// <param name="id">The id.</param>
 2040        /// <returns>BaseItem.</returns>
 2041        public BaseItem RetrieveItem(Guid id)
 2042        {
 2572043            return _itemRepository.RetrieveItem(id);
 2044        }
 2045
 2046        public List<Folder> GetCollectionFolders(BaseItem item)
 2047        {
 6162048            return GetCollectionFolders(item, GetUserRootFolder().Children.OfType<Folder>());
 2049        }
 2050
 2051        public List<Folder> GetCollectionFolders(BaseItem item, IEnumerable<Folder> allUserRootChildren)
 2052        {
 6282053            while (item is not null)
 2054            {
 6282055                var parent = item.GetParent();
 2056
 6282057                if (parent is AggregateFolder)
 2058                {
 2059                    break;
 2060                }
 2061
 5922062                if (parent is null)
 2063                {
 5802064                    var owner = item.GetOwner();
 2065
 5802066                    if (owner is null)
 2067                    {
 2068                        break;
 2069                    }
 2070
 02071                    item = owner;
 2072                }
 2073                else
 2074                {
 122075                    item = parent;
 2076                }
 2077            }
 2078
 6162079            if (item is null)
 2080            {
 02081                return new List<Folder>();
 2082            }
 2083
 6162084            return GetCollectionFoldersInternal(item, allUserRootChildren);
 2085        }
 2086
 2087        private static List<Folder> GetCollectionFoldersInternal(BaseItem item, IEnumerable<Folder> allUserRootChildren)
 2088        {
 6162089            return allUserRootChildren
 6162090                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.
 6162091                .ToList();
 2092        }
 2093
 2094        public LibraryOptions GetLibraryOptions(BaseItem item)
 2095        {
 4062096            if (item is CollectionFolder collectionFolder)
 2097            {
 182098                return collectionFolder.GetLibraryOptions();
 2099            }
 2100
 2101            // List.Find is more performant than FirstOrDefault due to enumerator allocation
 3882102            return GetCollectionFolders(item)
 3882103                .Find(folder => folder is CollectionFolder) is CollectionFolder collectionFolder2
 3882104                ? collectionFolder2.GetLibraryOptions()
 3882105                : new LibraryOptions();
 2106        }
 2107
 2108        public CollectionType? GetContentType(BaseItem item)
 2109        {
 612110            var configuredContentType = GetConfiguredContentType(item, false);
 612111            if (configuredContentType is not null)
 2112            {
 02113                return configuredContentType;
 2114            }
 2115
 612116            configuredContentType = GetConfiguredContentType(item, true);
 612117            if (configuredContentType is not null)
 2118            {
 02119                return configuredContentType;
 2120            }
 2121
 612122            return GetInheritedContentType(item);
 2123        }
 2124
 2125        public CollectionType? GetInheritedContentType(BaseItem item)
 2126        {
 612127            var type = GetTopFolderContentType(item);
 2128
 612129            if (type is not null)
 2130            {
 02131                return type;
 2132            }
 2133
 612134            return item.GetParents()
 612135                .Select(GetConfiguredContentType)
 612136                .LastOrDefault(i => i is not null);
 2137        }
 2138
 2139        public CollectionType? GetConfiguredContentType(BaseItem item)
 2140        {
 02141            return GetConfiguredContentType(item, false);
 2142        }
 2143
 2144        public CollectionType? GetConfiguredContentType(string path)
 2145        {
 02146            return GetContentTypeOverride(path, false);
 2147        }
 2148
 2149        public CollectionType? GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath)
 2150        {
 1222151            if (item is ICollectionFolder collectionFolder)
 2152            {
 02153                return collectionFolder.CollectionType;
 2154            }
 2155
 1222156            return GetContentTypeOverride(item.ContainingFolderPath, inheritConfiguredPath);
 2157        }
 2158
 2159        private CollectionType? GetContentTypeOverride(string path, bool inherit)
 2160        {
 1272161            var nameValuePair = _configurationManager.Configuration.ContentTypes
 1272162                                    .FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path)
 1272163                                                         || (inherit && !string.IsNullOrEmpty(i.Name)
 1272164                                                                     && _fileSystem.ContainsSubPath(i.Name, path)));
 1272165            if (Enum.TryParse<CollectionType>(nameValuePair?.Value, out var collectionType))
 2166            {
 02167                return collectionType;
 2168            }
 2169
 1272170            return null;
 2171        }
 2172
 2173        private CollectionType? GetTopFolderContentType(BaseItem item)
 2174        {
 612175            if (item is null)
 2176            {
 02177                return null;
 2178            }
 2179
 612180            while (!item.ParentId.IsEmpty())
 2181            {
 02182                var parent = item.GetParent();
 02183                if (parent is null || parent is AggregateFolder)
 2184                {
 2185                    break;
 2186                }
 2187
 02188                item = parent;
 2189            }
 2190
 612191            return GetUserRootFolder().Children
 612192                .OfType<ICollectionFolder>()
 612193                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.
 612194                .Select(i => i.CollectionType)
 612195                .FirstOrDefault(i => i is not null);
 2196        }
 2197
 2198        public UserView GetNamedView(
 2199            User user,
 2200            string name,
 2201            CollectionType? viewType,
 2202            string sortName)
 2203        {
 02204            return GetNamedView(user, name, Guid.Empty, viewType, sortName);
 2205        }
 2206
 2207        public UserView GetNamedView(
 2208            string name,
 2209            CollectionType viewType,
 2210            string sortName)
 2211        {
 02212            var path = Path.Combine(
 02213                _configurationManager.ApplicationPaths.InternalMetadataPath,
 02214                "views",
 02215                _fileSystem.GetValidFilename(viewType.ToString()));
 2216
 02217            var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView));
 2218
 02219            var item = GetItemById(id) as UserView;
 2220
 02221            var refresh = false;
 2222
 02223            if (item is null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase))
 2224            {
 02225                Directory.CreateDirectory(path);
 2226
 02227                item = new UserView
 02228                {
 02229                    Path = path,
 02230                    Id = id,
 02231                    DateCreated = DateTime.UtcNow,
 02232                    Name = name,
 02233                    ViewType = viewType,
 02234                    ForcedSortName = sortName
 02235                };
 2236
 02237                CreateItem(item, null);
 2238
 02239                refresh = true;
 2240            }
 2241
 02242            if (refresh)
 2243            {
 02244                item.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResu
 02245                ProviderManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), Ref
 2246            }
 2247
 02248            return item;
 2249        }
 2250
 2251        public UserView GetNamedView(
 2252            User user,
 2253            string name,
 2254            Guid parentId,
 2255            CollectionType? viewType,
 2256            string sortName)
 2257        {
 02258            var parentIdString = parentId.IsEmpty()
 02259                ? null
 02260                : parentId.ToString("N", CultureInfo.InvariantCulture);
 02261            var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdStrin
 2262
 02263            var id = GetNewItemId(idValues, typeof(UserView));
 2264
 02265            var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N
 2266
 02267            var item = GetItemById(id) as UserView;
 2268
 02269            var isNew = false;
 2270
 02271            if (item is null)
 2272            {
 02273                Directory.CreateDirectory(path);
 2274
 02275                item = new UserView
 02276                {
 02277                    Path = path,
 02278                    Id = id,
 02279                    DateCreated = DateTime.UtcNow,
 02280                    Name = name,
 02281                    ViewType = viewType,
 02282                    ForcedSortName = sortName,
 02283                    UserId = user.Id,
 02284                    DisplayParentId = parentId
 02285                };
 2286
 02287                CreateItem(item, null);
 2288
 02289                isNew = true;
 2290            }
 2291
 02292            var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
 2293
 02294            if (!refresh && !item.DisplayParentId.IsEmpty())
 2295            {
 02296                var displayParent = GetItemById(item.DisplayParentId);
 02297                refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed;
 2298            }
 2299
 02300            if (refresh)
 2301            {
 02302                ProviderManager.QueueRefresh(
 02303                    item.Id,
 02304                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 02305                    {
 02306                        // Need to force save to increment DateLastSaved
 02307                        ForceSave = true
 02308                    },
 02309                    RefreshPriority.Normal);
 2310            }
 2311
 02312            return item;
 2313        }
 2314
 2315        public UserView GetShadowView(
 2316            BaseItem parent,
 2317            CollectionType? viewType,
 2318            string sortName)
 2319        {
 02320            ArgumentNullException.ThrowIfNull(parent);
 2321
 02322            var name = parent.Name;
 02323            var parentId = parent.Id;
 2324
 02325            var idValues = "38_namedview_" + name + parentId + (viewType?.ToString() ?? string.Empty);
 2326
 02327            var id = GetNewItemId(idValues, typeof(UserView));
 2328
 02329            var path = parent.Path;
 2330
 02331            var item = GetItemById(id) as UserView;
 2332
 02333            var isNew = false;
 2334
 02335            if (item is null)
 2336            {
 02337                Directory.CreateDirectory(path);
 2338
 02339                item = new UserView
 02340                {
 02341                    Path = path,
 02342                    Id = id,
 02343                    DateCreated = DateTime.UtcNow,
 02344                    Name = name,
 02345                    ViewType = viewType,
 02346                    ForcedSortName = sortName
 02347                };
 2348
 02349                item.DisplayParentId = parentId;
 2350
 02351                CreateItem(item, null);
 2352
 02353                isNew = true;
 2354            }
 2355
 02356            var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
 2357
 02358            if (!refresh && !item.DisplayParentId.IsEmpty())
 2359            {
 02360                var displayParent = GetItemById(item.DisplayParentId);
 02361                refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed;
 2362            }
 2363
 02364            if (refresh)
 2365            {
 02366                ProviderManager.QueueRefresh(
 02367                    item.Id,
 02368                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 02369                    {
 02370                        // Need to force save to increment DateLastSaved
 02371                        ForceSave = true
 02372                    },
 02373                    RefreshPriority.Normal);
 2374            }
 2375
 02376            return item;
 2377        }
 2378
 2379        public UserView GetNamedView(
 2380            string name,
 2381            Guid parentId,
 2382            CollectionType? viewType,
 2383            string sortName,
 2384            string uniqueId)
 2385        {
 02386            ArgumentException.ThrowIfNullOrEmpty(name);
 2387
 02388            var parentIdString = parentId.IsEmpty()
 02389                ? null
 02390                : parentId.ToString("N", CultureInfo.InvariantCulture);
 02391            var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.E
 02392            if (!string.IsNullOrEmpty(uniqueId))
 2393            {
 02394                idValues += uniqueId;
 2395            }
 2396
 02397            var id = GetNewItemId(idValues, typeof(UserView));
 2398
 02399            var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N
 2400
 02401            var item = GetItemById(id) as UserView;
 2402
 02403            var isNew = false;
 2404
 02405            if (item is null)
 2406            {
 02407                Directory.CreateDirectory(path);
 2408
 02409                item = new UserView
 02410                {
 02411                    Path = path,
 02412                    Id = id,
 02413                    DateCreated = DateTime.UtcNow,
 02414                    Name = name,
 02415                    ViewType = viewType,
 02416                    ForcedSortName = sortName
 02417                };
 2418
 02419                item.DisplayParentId = parentId;
 2420
 02421                CreateItem(item, null);
 2422
 02423                isNew = true;
 2424            }
 2425
 02426            if (viewType != item.ViewType)
 2427            {
 02428                item.ViewType = viewType;
 02429                item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult
 2430            }
 2431
 02432            var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
 2433
 02434            if (!refresh && !item.DisplayParentId.IsEmpty())
 2435            {
 02436                var displayParent = GetItemById(item.DisplayParentId);
 02437                refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed;
 2438            }
 2439
 02440            if (refresh)
 2441            {
 02442                ProviderManager.QueueRefresh(
 02443                    item.Id,
 02444                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 02445                    {
 02446                        // Need to force save to increment DateLastSaved
 02447                        ForceSave = true
 02448                    },
 02449                    RefreshPriority.Normal);
 2450            }
 2451
 02452            return item;
 2453        }
 2454
 2455        public BaseItem GetParentItem(Guid? parentId, Guid? userId)
 2456        {
 32457            if (parentId.HasValue)
 2458            {
 02459                return GetItemById(parentId.Value) ?? throw new ArgumentException($"Invalid parent id: {parentId.Value}"
 2460            }
 2461
 32462            if (!userId.IsNullOrEmpty())
 2463            {
 32464                return GetUserRootFolder();
 2465            }
 2466
 02467            return RootFolder;
 2468        }
 2469
 2470        /// <inheritdoc />
 2471        public void QueueLibraryScan()
 2472        {
 02473            _taskManager.QueueScheduledTask<RefreshMediaLibraryTask>();
 02474        }
 2475
 2476        /// <inheritdoc />
 2477        public int? GetSeasonNumberFromPath(string path)
 02478            => SeasonPathParser.Parse(path, true, true).SeasonNumber;
 2479
 2480        /// <inheritdoc />
 2481        public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
 2482        {
 02483            var series = episode.Series;
 02484            bool? isAbsoluteNaming = series is not null && string.Equals(series.DisplayOrder, "absolute", StringComparis
 02485            if (!isAbsoluteNaming.Value)
 2486            {
 2487                // In other words, no filter applied
 02488                isAbsoluteNaming = null;
 2489            }
 2490
 02491            var resolver = new EpisodeResolver(_namingOptions);
 2492
 02493            var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
 2494
 2495            // TODO nullable - what are we trying to do there with empty episodeInfo?
 02496            EpisodeInfo? episodeInfo = null;
 02497            if (episode.IsFileProtocol)
 2498            {
 02499                episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming);
 2500                // Resolve from parent folder if it's not the Season folder
 02501                var parent = episode.GetParent();
 02502                if (episodeInfo is null && parent.GetType() == typeof(Folder))
 2503                {
 02504                    episodeInfo = resolver.Resolve(parent.Path, true, null, null, isAbsoluteNaming);
 02505                    if (episodeInfo is not null)
 2506                    {
 2507                        // add the container
 02508                        episodeInfo.Container = Path.GetExtension(episode.Path)?.TrimStart('.');
 2509                    }
 2510                }
 2511            }
 2512
 02513            episodeInfo ??= new EpisodeInfo(episode.Path);
 2514
 2515            try
 2516            {
 02517                var libraryOptions = GetLibraryOptions(episode);
 02518                if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringCompa
 2519                {
 2520                    // Read from metadata
 02521                    var mediaInfo = _mediaEncoder.GetMediaInfo(
 02522                        new MediaInfoRequest
 02523                        {
 02524                            MediaSource = episode.GetMediaSources(false)[0],
 02525                            MediaType = DlnaProfileType.Video
 02526                        },
 02527                        CancellationToken.None).GetAwaiter().GetResult();
 02528                    if (mediaInfo.ParentIndexNumber > 0)
 2529                    {
 02530                        episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber;
 2531                    }
 2532
 02533                    if (mediaInfo.IndexNumber > 0)
 2534                    {
 02535                        episodeInfo.EpisodeNumber = mediaInfo.IndexNumber;
 2536                    }
 2537
 02538                    if (!string.IsNullOrEmpty(mediaInfo.ShowName))
 2539                    {
 02540                        episodeInfo.SeriesName = mediaInfo.ShowName;
 2541                    }
 2542                }
 02543            }
 02544            catch (Exception ex)
 2545            {
 02546                _logger.LogError(ex, "Error reading the episode information with ffprobe. Episode: {EpisodeInfo}", episo
 02547            }
 2548
 02549            var changed = false;
 2550
 02551            if (episodeInfo.IsByDate)
 2552            {
 02553                if (episode.IndexNumber.HasValue)
 2554                {
 02555                    episode.IndexNumber = null;
 02556                    changed = true;
 2557                }
 2558
 02559                if (episode.IndexNumberEnd.HasValue)
 2560                {
 02561                    episode.IndexNumberEnd = null;
 02562                    changed = true;
 2563                }
 2564
 02565                if (!episode.PremiereDate.HasValue)
 2566                {
 02567                    if (episodeInfo.Year.HasValue && episodeInfo.Month.HasValue && episodeInfo.Day.HasValue)
 2568                    {
 02569                        episode.PremiereDate = new DateTime(episodeInfo.Year.Value, episodeInfo.Month.Value, episodeInfo
 2570                    }
 2571
 02572                    if (episode.PremiereDate.HasValue)
 2573                    {
 02574                        changed = true;
 2575                    }
 2576                }
 2577
 02578                if (!episode.ProductionYear.HasValue)
 2579                {
 02580                    episode.ProductionYear = episodeInfo.Year;
 2581
 02582                    if (episode.ProductionYear.HasValue)
 2583                    {
 02584                        changed = true;
 2585                    }
 2586                }
 2587            }
 2588            else
 2589            {
 02590                if (!episode.IndexNumber.HasValue || forceRefresh)
 2591                {
 02592                    if (episode.IndexNumber != episodeInfo.EpisodeNumber)
 2593                    {
 02594                        changed = true;
 2595                    }
 2596
 02597                    episode.IndexNumber = episodeInfo.EpisodeNumber;
 2598                }
 2599
 02600                if (!episode.IndexNumberEnd.HasValue || forceRefresh)
 2601                {
 02602                    if (episode.IndexNumberEnd != episodeInfo.EndingEpisodeNumber)
 2603                    {
 02604                        changed = true;
 2605                    }
 2606
 02607                    episode.IndexNumberEnd = episodeInfo.EndingEpisodeNumber;
 2608                }
 2609
 02610                if (!episode.ParentIndexNumber.HasValue || forceRefresh)
 2611                {
 02612                    if (episode.ParentIndexNumber != episodeInfo.SeasonNumber)
 2613                    {
 02614                        changed = true;
 2615                    }
 2616
 02617                    episode.ParentIndexNumber = episodeInfo.SeasonNumber;
 2618                }
 2619            }
 2620
 02621            if (!episode.ParentIndexNumber.HasValue)
 2622            {
 02623                var season = episode.Season;
 2624
 02625                if (season is not null)
 2626                {
 02627                    episode.ParentIndexNumber = season.IndexNumber;
 2628                }
 2629                else
 2630                {
 2631                    /*
 2632                    Anime series don't generally have a season in their file name, however,
 2633                    TVDb needs a season to correctly get the metadata.
 2634                    Hence, a null season needs to be filled with something. */
 2635                    // FIXME perhaps this would be better for TVDb parser to ask for season 1 if no season is specified
 02636                    episode.ParentIndexNumber = 1;
 2637                }
 2638
 02639                if (episode.ParentIndexNumber.HasValue)
 2640                {
 02641                    changed = true;
 2642                }
 2643            }
 2644
 02645            return changed;
 2646        }
 2647
 2648        public ItemLookupInfo ParseName(string name)
 2649        {
 02650            var namingOptions = _namingOptions;
 02651            var result = VideoResolver.CleanDateTime(name, namingOptions);
 2652
 02653            return new ItemLookupInfo
 02654            {
 02655                Name = VideoResolver.TryCleanString(result.Name, namingOptions, out var newName) ? newName : result.Name
 02656                Year = result.Year
 02657            };
 2658        }
 2659
 2660        public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, ID
 2661        {
 2662            var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions);
 2663            if (ownerVideoInfo is null)
 2664            {
 2665                yield break;
 2666            }
 2667
 2668            var count = fileSystemChildren.Count;
 2669            for (var i = 0; i < count; i++)
 2670            {
 2671                var current = fileSystemChildren[i];
 2672                if (current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name))
 2673                {
 2674                    var filesInSubFolder = _fileSystem.GetFiles(current.FullName, null, false, false);
 2675                    foreach (var file in filesInSubFolder)
 2676                    {
 2677                        if (!_extraResolver.TryGetExtraTypeForOwner(file.FullName, ownerVideoInfo, out var extraType))
 2678                        {
 2679                            continue;
 2680                        }
 2681
 2682                        var extra = GetExtra(file, extraType.Value);
 2683                        if (extra is not null)
 2684                        {
 2685                            yield return extra;
 2686                        }
 2687                    }
 2688                }
 2689                else if (!current.IsDirectory && _extraResolver.TryGetExtraTypeForOwner(current.FullName, ownerVideoInfo
 2690                {
 2691                    var extra = GetExtra(current, extraType.Value);
 2692                    if (extra is not null)
 2693                    {
 2694                        yield return extra;
 2695                    }
 2696                }
 2697            }
 2698
 2699            BaseItem? GetExtra(FileSystemMetadata file, ExtraType extraType)
 2700            {
 2701                var extra = ResolvePath(_fileSystem.GetFileInfo(file.FullName), directoryService, _extraResolver.GetReso
 2702                if (extra is not Video && extra is not Audio)
 2703                {
 2704                    return null;
 2705                }
 2706
 2707                // Try to retrieve it from the db. If we don't find it, use the resolved version
 2708                var itemById = GetItemById(extra.Id);
 2709                if (itemById is not null)
 2710                {
 2711                    extra = itemById;
 2712                }
 2713
 2714                // Only update extra type if it is more specific then the currently known extra type
 2715                if (extra.ExtraType is null or ExtraType.Unknown || extraType != ExtraType.Unknown)
 2716                {
 2717                    extra.ExtraType = extraType;
 2718                }
 2719
 2720                extra.ParentId = Guid.Empty;
 2721                extra.OwnerId = owner.Id;
 2722                return extra;
 2723            }
 2724        }
 2725
 2726        public string GetPathAfterNetworkSubstitution(string path, BaseItem? ownerItem)
 2727        {
 222728            foreach (var map in _configurationManager.Configuration.PathSubstitutions)
 2729            {
 02730                if (path.TryReplaceSubPath(map.From, map.To, out var newPath))
 2731                {
 02732                    return newPath;
 2733                }
 2734            }
 2735
 112736            return path;
 2737        }
 2738
 2739        public List<PersonInfo> GetPeople(InternalPeopleQuery query)
 2740        {
 02741            return _itemRepository.GetPeople(query);
 2742        }
 2743
 2744        public List<PersonInfo> GetPeople(BaseItem item)
 2745        {
 572746            if (item.SupportsPeople)
 2747            {
 02748                var people = GetPeople(new InternalPeopleQuery
 02749                {
 02750                    ItemId = item.Id
 02751                });
 2752
 02753                if (people.Count > 0)
 2754                {
 02755                    return people;
 2756                }
 2757            }
 2758
 572759            return new List<PersonInfo>();
 2760        }
 2761
 2762        public List<Person> GetPeopleItems(InternalPeopleQuery query)
 2763        {
 02764            return _itemRepository.GetPeopleNames(query)
 02765            .Select(i =>
 02766            {
 02767                try
 02768                {
 02769                    return GetPerson(i);
 02770                }
 02771                catch (Exception ex)
 02772                {
 02773                    _logger.LogError(ex, "Error getting person");
 02774                    return null;
 02775                }
 02776            })
 02777            .Where(i => i is not null)
 02778            .Where(i => query.User is null || i!.IsVisible(query.User))
 02779            .ToList()!; // null values are filtered out
 2780        }
 2781
 2782        public List<string> GetPeopleNames(InternalPeopleQuery query)
 2783        {
 02784            return _itemRepository.GetPeopleNames(query);
 2785        }
 2786
 2787        public void UpdatePeople(BaseItem item, List<PersonInfo> people)
 2788        {
 02789            UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult();
 02790        }
 2791
 2792        /// <inheritdoc />
 2793        public async Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken)
 2794        {
 2795            if (!item.SupportsPeople)
 2796            {
 2797                return;
 2798            }
 2799
 2800            _itemRepository.UpdatePeople(item.Id, people);
 2801            if (people is not null)
 2802            {
 2803                await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
 2804            }
 2805        }
 2806
 2807        public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex, bool re
 2808        {
 2809            foreach (var url in image.Path.Split('|'))
 2810            {
 2811                try
 2812                {
 2813                    _logger.LogDebug("ConvertImageToLocal item {0} - image url: {1}", item.Id, url);
 2814
 2815                    await ProviderManager.SaveImage(item, url, image.Type, imageIndex, CancellationToken.None).Configure
 2816
 2817                    await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwai
 2818
 2819                    return item.GetImageInfo(image.Type, imageIndex);
 2820                }
 2821                catch (HttpRequestException ex)
 2822                {
 2823                    if (ex.StatusCode.HasValue
 2824                        && (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forb
 2825                    {
 2826                        _logger.LogDebug(ex, "Error downloading image {Url}", url);
 2827                        continue;
 2828                    }
 2829
 2830                    throw;
 2831                }
 2832            }
 2833
 2834            if (removeOnFailure)
 2835            {
 2836                // Remove this image to prevent it from retrying over and over
 2837                item.RemoveImage(image);
 2838                await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(fa
 2839            }
 2840
 2841            throw new InvalidOperationException("Unable to convert any images to local");
 2842        }
 2843
 2844        public async Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, b
 2845        {
 2846            if (string.IsNullOrWhiteSpace(name))
 2847            {
 2848                throw new ArgumentNullException(nameof(name));
 2849            }
 2850
 2851            name = _fileSystem.GetValidFilename(name);
 2852
 2853            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 2854
 2855            var existingNameCount = 1; // first numbered name will be 2
 2856            var virtualFolderPath = Path.Combine(rootFolderPath, name);
 2857            var originalName = name;
 2858            while (Directory.Exists(virtualFolderPath))
 2859            {
 2860                existingNameCount++;
 2861                name = originalName + existingNameCount;
 2862                virtualFolderPath = Path.Combine(rootFolderPath, name);
 2863            }
 2864
 2865            var mediaPathInfos = options.PathInfos;
 2866            if (mediaPathInfos is not null)
 2867            {
 2868                var invalidpath = mediaPathInfos.FirstOrDefault(i => !Directory.Exists(i.Path));
 2869                if (invalidpath is not null)
 2870                {
 2871                    throw new ArgumentException("The specified path does not exist: " + invalidpath.Path + ".");
 2872                }
 2873            }
 2874
 2875            LibraryMonitor.Stop();
 2876
 2877            try
 2878            {
 2879                Directory.CreateDirectory(virtualFolderPath);
 2880
 2881                if (collectionType is not null)
 2882                {
 2883                    var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collect
 2884
 2885                    await File.WriteAllBytesAsync(path, Array.Empty<byte>()).ConfigureAwait(false);
 2886                }
 2887
 2888                CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
 2889
 2890                if (mediaPathInfos is not null)
 2891                {
 2892                    foreach (var path in mediaPathInfos)
 2893                    {
 2894                        AddMediaPathInternal(name, path, false);
 2895                    }
 2896                }
 2897            }
 2898            finally
 2899            {
 2900                if (refreshLibrary)
 2901                {
 2902                    await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false);
 2903
 2904                    StartScanInBackground();
 2905                }
 2906                else
 2907                {
 2908                    // Need to add a delay here or directory watchers may still pick up the changes
 2909                    await Task.Delay(1000).ConfigureAwait(false);
 2910                    LibraryMonitor.Start();
 2911                }
 2912            }
 2913        }
 2914
 2915        private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
 2916        {
 2917            List<BaseItem>? personsToSave = null;
 2918
 2919            foreach (var person in people)
 2920            {
 2921                cancellationToken.ThrowIfCancellationRequested();
 2922
 2923                var itemUpdateType = ItemUpdateType.MetadataDownload;
 2924                var saveEntity = false;
 2925                var personEntity = GetPerson(person.Name);
 2926
 2927                if (personEntity is null)
 2928                {
 2929                    var path = Person.GetPath(person.Name);
 2930                    personEntity = new Person()
 2931                    {
 2932                        Name = person.Name,
 2933                        Id = GetItemByNameId<Person>(path),
 2934                        DateCreated = DateTime.UtcNow,
 2935                        DateModified = DateTime.UtcNow,
 2936                        Path = path
 2937                    };
 2938
 2939                    personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
 2940                    saveEntity = true;
 2941                }
 2942
 2943                foreach (var id in person.ProviderIds)
 2944                {
 2945                    if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase)
 2946                    {
 2947                        personEntity.SetProviderId(id.Key, id.Value);
 2948                        saveEntity = true;
 2949                    }
 2950                }
 2951
 2952                if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary))
 2953                {
 2954                    personEntity.SetImage(
 2955                        new ItemImageInfo
 2956                        {
 2957                            Path = person.ImageUrl,
 2958                            Type = ImageType.Primary
 2959                        },
 2960                        0);
 2961
 2962                    saveEntity = true;
 2963                    itemUpdateType = ItemUpdateType.ImageUpdate;
 2964                }
 2965
 2966                if (saveEntity)
 2967                {
 2968                    (personsToSave ??= new()).Add(personEntity);
 2969                    await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
 2970                }
 2971            }
 2972
 2973            if (personsToSave is not null)
 2974            {
 2975                CreateItems(personsToSave, null, CancellationToken.None);
 2976            }
 2977        }
 2978
 2979        private void StartScanInBackground()
 2980        {
 22981            Task.Run(() =>
 22982            {
 22983                // No need to start if scanning the library because it will handle it
 22984                ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
 22985            });
 22986        }
 2987
 2988        public void AddMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
 2989        {
 12990            AddMediaPathInternal(virtualFolderName, mediaPath, true);
 02991        }
 2992
 2993        private void AddMediaPathInternal(string virtualFolderName, MediaPathInfo pathInfo, bool saveLibraryOptions)
 2994        {
 12995            ArgumentNullException.ThrowIfNull(pathInfo);
 2996
 12997            var path = pathInfo.Path;
 2998
 12999            if (string.IsNullOrWhiteSpace(path))
 3000            {
 03001                throw new ArgumentException(nameof(path));
 3002            }
 3003
 13004            if (!Directory.Exists(path))
 3005            {
 13006                throw new FileNotFoundException("The path does not exist.");
 3007            }
 3008
 03009            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 03010            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3011
 03012            var shortcutFilename = Path.GetFileNameWithoutExtension(path);
 3013
 03014            var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
 3015
 03016            while (File.Exists(lnk))
 3017            {
 03018                shortcutFilename += "1";
 03019                lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
 3020            }
 3021
 03022            _fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
 3023
 03024            RemoveContentTypeOverrides(path);
 3025
 03026            if (saveLibraryOptions)
 3027            {
 03028                var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3029
 03030                libraryOptions.PathInfos = [..libraryOptions.PathInfos, pathInfo];
 3031
 03032                SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
 3033
 03034                CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 3035            }
 03036        }
 3037
 3038        public void UpdateMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
 3039        {
 03040            ArgumentNullException.ThrowIfNull(mediaPath);
 3041
 03042            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 03043            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3044
 03045            var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3046
 03047            SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
 3048
 03049            CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 03050        }
 3051
 3052        private void SyncLibraryOptionsToLocations(string virtualFolderPath, LibraryOptions options)
 3053        {
 03054            var topLibraryFolders = GetUserRootFolder().Children.ToList();
 03055            var info = GetVirtualFolderInfo(virtualFolderPath, topLibraryFolders, null);
 3056
 03057            if (info.Locations.Length > 0 && info.Locations.Length != options.PathInfos.Length)
 3058            {
 03059                var list = options.PathInfos.ToList();
 3060
 03061                foreach (var location in info.Locations)
 3062                {
 03063                    if (!list.Any(i => string.Equals(i.Path, location, StringComparison.Ordinal)))
 3064                    {
 03065                        list.Add(new MediaPathInfo(location));
 3066                    }
 3067                }
 3068
 03069                options.PathInfos = list.ToArray();
 3070            }
 03071        }
 3072
 3073        public async Task RemoveVirtualFolder(string name, bool refreshLibrary)
 3074        {
 3075            if (string.IsNullOrWhiteSpace(name))
 3076            {
 3077                throw new ArgumentNullException(nameof(name));
 3078            }
 3079
 3080            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 3081
 3082            var path = Path.Combine(rootFolderPath, name);
 3083
 3084            if (!Directory.Exists(path))
 3085            {
 3086                throw new FileNotFoundException("The media folder does not exist");
 3087            }
 3088
 3089            LibraryMonitor.Stop();
 3090
 3091            try
 3092            {
 3093                Directory.Delete(path, true);
 3094            }
 3095            finally
 3096            {
 3097                CollectionFolder.OnCollectionFolderChange();
 3098
 3099                if (refreshLibrary)
 3100                {
 3101                    await ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
 3102
 3103                    StartScanInBackground();
 3104                }
 3105                else
 3106                {
 3107                    // Need to add a delay here or directory watchers may still pick up the changes
 3108                    await Task.Delay(1000).ConfigureAwait(false);
 3109                    LibraryMonitor.Start();
 3110                }
 3111            }
 3112        }
 3113
 3114        private void RemoveContentTypeOverrides(string path)
 3115        {
 03116            if (string.IsNullOrWhiteSpace(path))
 3117            {
 03118                throw new ArgumentNullException(nameof(path));
 3119            }
 3120
 03121            List<NameValuePair>? removeList = null;
 3122
 03123            foreach (var contentType in _configurationManager.Configuration.ContentTypes)
 3124            {
 03125                if (string.IsNullOrWhiteSpace(contentType.Name)
 03126                    || _fileSystem.AreEqual(path, contentType.Name)
 03127                    || _fileSystem.ContainsSubPath(path, contentType.Name))
 3128                {
 03129                    (removeList ??= new()).Add(contentType);
 3130                }
 3131            }
 3132
 03133            if (removeList is not null)
 3134            {
 03135                _configurationManager.Configuration.ContentTypes = _configurationManager.Configuration.ContentTypes
 03136                    .Except(removeList)
 03137                    .ToArray();
 3138
 03139                _configurationManager.SaveConfiguration();
 3140            }
 03141        }
 3142
 3143        public void RemoveMediaPath(string virtualFolderName, string mediaPath)
 3144        {
 13145            ArgumentException.ThrowIfNullOrEmpty(mediaPath);
 3146
 13147            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 13148            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3149
 13150            if (!Directory.Exists(virtualFolderPath))
 3151            {
 13152                throw new FileNotFoundException(
 13153                    string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolder
 3154            }
 3155
 03156            var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
 03157                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCa
 03158                .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, String
 3159
 03160            if (!string.IsNullOrEmpty(shortcut))
 3161            {
 03162                _fileSystem.DeleteFile(shortcut);
 3163            }
 3164
 03165            var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3166
 03167            libraryOptions.PathInfos = libraryOptions
 03168                .PathInfos
 03169                .Where(i => !string.Equals(i.Path, mediaPath, StringComparison.Ordinal))
 03170                .ToArray();
 3171
 03172            CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 03173        }
 3174
 3175        private static bool ItemIsVisible(BaseItem? item, User? user)
 3176        {
 253177            if (item is null)
 3178            {
 213179                return false;
 3180            }
 3181
 43182            if (user is null)
 3183            {
 03184                return true;
 3185            }
 3186
 43187            return item is UserRootFolder || item.IsVisibleStandalone(user);
 3188        }
 3189    }
 3190}

Methods/Properties

.ctor(MediaBrowser.Controller.IServerApplicationHost,Microsoft.Extensions.Logging.ILoggerFactory,MediaBrowser.Model.Tasks.ITaskManager,MediaBrowser.Controller.Library.IUserManager,MediaBrowser.Controller.Configuration.IServerConfigurationManager,MediaBrowser.Controller.Library.IUserDataManager,System.Lazy`1<MediaBrowser.Controller.Library.ILibraryMonitor>,MediaBrowser.Model.IO.IFileSystem,System.Lazy`1<MediaBrowser.Controller.Providers.IProviderManager>,System.Lazy`1<MediaBrowser.Controller.Library.IUserViewManager>,MediaBrowser.Controller.MediaEncoding.IMediaEncoder,MediaBrowser.Controller.Persistence.IItemRepository,MediaBrowser.Controller.Drawing.IImageProcessor,Emby.Naming.Common.NamingOptions,MediaBrowser.Controller.Providers.IDirectoryService)
get_RootFolder()
get_LibraryMonitor()
get_ProviderManager()
get_UserViewManager()
AddParts(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Resolvers.IResolverIgnoreRule>,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Resolvers.IItemResolver>,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Library.IIntroProvider>,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Sorting.IBaseItemComparer>,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Library.ILibraryPostScanTask>)
RecordConfigurationValues(MediaBrowser.Model.Configuration.ServerConfiguration)
ConfigurationUpdated(System.Object,System.EventArgs)
RegisterItem(MediaBrowser.Controller.Entities.BaseItem)
DeleteItem(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Library.DeleteOptions)
DeleteItem(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Library.DeleteOptions,System.Boolean)
DeleteItem(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Library.DeleteOptions,MediaBrowser.Controller.Entities.BaseItem,System.Boolean)
GetMetadataPaths(MediaBrowser.Controller.Entities.BaseItem,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.BaseItem>)
ResolveItem(MediaBrowser.Controller.Library.ItemResolveArgs,MediaBrowser.Controller.Resolvers.IItemResolver[])
Resolve(MediaBrowser.Controller.Library.ItemResolveArgs,MediaBrowser.Controller.Resolvers.IItemResolver)
GetNewItemId(System.String,System.Type)
GetNewItemIdInternal(System.String,System.Type,System.Boolean)
ResolvePath(MediaBrowser.Model.IO.FileSystemMetadata,MediaBrowser.Controller.Entities.Folder,MediaBrowser.Controller.Providers.IDirectoryService)
ResolvePath(MediaBrowser.Model.IO.FileSystemMetadata,MediaBrowser.Controller.Providers.IDirectoryService,MediaBrowser.Controller.Resolvers.IItemResolver[],MediaBrowser.Controller.Entities.Folder,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>,MediaBrowser.Model.Configuration.LibraryOptions)
IgnoreFile(MediaBrowser.Model.IO.FileSystemMetadata,MediaBrowser.Controller.Entities.BaseItem)
NormalizeRootPathList(System.Collections.Generic.IEnumerable`1<MediaBrowser.Model.IO.FileSystemMetadata>)
ShouldResolvePathContents(MediaBrowser.Controller.Library.ItemResolveArgs)
ResolvePaths(System.Collections.Generic.IEnumerable`1<MediaBrowser.Model.IO.FileSystemMetadata>,MediaBrowser.Controller.Providers.IDirectoryService,MediaBrowser.Controller.Entities.Folder,MediaBrowser.Model.Configuration.LibraryOptions,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>)
ResolvePaths(System.Collections.Generic.IEnumerable`1<MediaBrowser.Model.IO.FileSystemMetadata>,MediaBrowser.Controller.Providers.IDirectoryService,MediaBrowser.Controller.Entities.Folder,MediaBrowser.Model.Configuration.LibraryOptions,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>,MediaBrowser.Controller.Resolvers.IItemResolver[])
CreateRootFolder()
GetUserRootFolder()
FindByPath(System.String,System.Nullable`1<System.Boolean>)
GetPerson(System.String)
GetStudio(System.String)
GetStudioId(System.String)
GetGenreId(System.String)
GetMusicGenreId(System.String)
GetGenre(System.String)
GetMusicGenre(System.String)
GetYear(System.Int32)
GetArtist(System.String)
GetArtist(System.String,MediaBrowser.Controller.Dto.DtoOptions)
CreateItemByName(System.Func`2<System.String,System.String>,System.String,MediaBrowser.Controller.Dto.DtoOptions)
GetItemByNameId(System.String)
ValidatePeopleAsync(System.IProgress`1<System.Double>,System.Threading.CancellationToken)
ValidateMediaLibrary(System.IProgress`1<System.Double>,System.Threading.CancellationToken)
GetVirtualFolders()
GetVirtualFolders(System.Boolean)
GetVirtualFolderInfo(System.String,System.Collections.Generic.List`1<MediaBrowser.Controller.Entities.BaseItem>,System.Collections.Generic.HashSet`1<System.Guid>)
GetCollectionType(System.String)
GetItemById(System.Guid)
GetItemById(System.Guid)
GetItemById(System.Guid,System.Guid)
GetItemById(System.Guid,Jellyfin.Data.Entities.User)
GetItemList(MediaBrowser.Controller.Entities.InternalItemsQuery,System.Boolean)
GetItemList(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetCount(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetItemList(MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.List`1<MediaBrowser.Controller.Entities.BaseItem>)
QueryItems(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetItemIds(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetStudios(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetGenres(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetMusicGenres(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetAllArtists(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetArtists(MediaBrowser.Controller.Entities.InternalItemsQuery)
SetTopParentOrAncestorIds(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetAlbumArtists(MediaBrowser.Controller.Entities.InternalItemsQuery)
GetItemsResult(MediaBrowser.Controller.Entities.InternalItemsQuery)
SetTopParentIdsOrAncestors(MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.IReadOnlyCollection`1<MediaBrowser.Controller.Entities.BaseItem>)
AddUserToQuery(MediaBrowser.Controller.Entities.InternalItemsQuery,Jellyfin.Data.Entities.User,System.Boolean)
GetTopParentIdsForQuery(MediaBrowser.Controller.Entities.BaseItem,Jellyfin.Data.Entities.User)
ResolveIntro(MediaBrowser.Controller.Library.IntroInfo)
Sort(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.BaseItem>,Jellyfin.Data.Entities.User,System.Collections.Generic.IEnumerable`1<Jellyfin.Data.Enums.ItemSortBy>,Jellyfin.Data.Enums.SortOrder)
Sort(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.BaseItem>,Jellyfin.Data.Entities.User,System.Collections.Generic.IEnumerable`1<System.ValueTuple`2<Jellyfin.Data.Enums.ItemSortBy,Jellyfin.Data.Enums.SortOrder>>)
GetComparer(Jellyfin.Data.Enums.ItemSortBy,Jellyfin.Data.Entities.User)
CreateItem(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Entities.BaseItem)
CreateItems(System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>,MediaBrowser.Controller.Entities.BaseItem,System.Threading.CancellationToken)
ImageNeedsRefresh(MediaBrowser.Controller.Entities.ItemImageInfo)
UpdateItemAsync(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Library.ItemUpdateType,System.Threading.CancellationToken)
ReportItemRemoved(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Entities.BaseItem)
RetrieveItem(System.Guid)
GetCollectionFolders(MediaBrowser.Controller.Entities.BaseItem)
GetCollectionFolders(MediaBrowser.Controller.Entities.BaseItem,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.Folder>)
GetCollectionFoldersInternal(MediaBrowser.Controller.Entities.BaseItem,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.Folder>)
GetLibraryOptions(MediaBrowser.Controller.Entities.BaseItem)
GetContentType(MediaBrowser.Controller.Entities.BaseItem)
GetInheritedContentType(MediaBrowser.Controller.Entities.BaseItem)
GetConfiguredContentType(MediaBrowser.Controller.Entities.BaseItem)
GetConfiguredContentType(System.String)
GetConfiguredContentType(MediaBrowser.Controller.Entities.BaseItem,System.Boolean)
GetContentTypeOverride(System.String,System.Boolean)
GetTopFolderContentType(MediaBrowser.Controller.Entities.BaseItem)
GetNamedView(Jellyfin.Data.Entities.User,System.String,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>,System.String)
GetNamedView(System.String,Jellyfin.Data.Enums.CollectionType,System.String)
GetNamedView(Jellyfin.Data.Entities.User,System.String,System.Guid,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>,System.String)
GetShadowView(MediaBrowser.Controller.Entities.BaseItem,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>,System.String)
GetNamedView(System.String,System.Guid,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>,System.String,System.String)
GetParentItem(System.Nullable`1<System.Guid>,System.Nullable`1<System.Guid>)
QueueLibraryScan()
GetSeasonNumberFromPath(System.String)
FillMissingEpisodeNumbersFromPath(MediaBrowser.Controller.Entities.TV.Episode,System.Boolean)
ParseName(System.String)
GetPathAfterNetworkSubstitution(System.String,MediaBrowser.Controller.Entities.BaseItem)
GetPeople(MediaBrowser.Controller.Entities.InternalPeopleQuery)
GetPeople(MediaBrowser.Controller.Entities.BaseItem)
GetPeopleItems(MediaBrowser.Controller.Entities.InternalPeopleQuery)
GetPeopleNames(MediaBrowser.Controller.Entities.InternalPeopleQuery)
UpdatePeople(MediaBrowser.Controller.Entities.BaseItem,System.Collections.Generic.List`1<MediaBrowser.Controller.Entities.PersonInfo>)
StartScanInBackground()
AddMediaPath(System.String,MediaBrowser.Model.Configuration.MediaPathInfo)
AddMediaPathInternal(System.String,MediaBrowser.Model.Configuration.MediaPathInfo,System.Boolean)
UpdateMediaPath(System.String,MediaBrowser.Model.Configuration.MediaPathInfo)
SyncLibraryOptionsToLocations(System.String,MediaBrowser.Model.Configuration.LibraryOptions)
RemoveContentTypeOverrides(System.String)
RemoveMediaPath(System.String,System.String)
ItemIsVisible(MediaBrowser.Controller.Entities.BaseItem,Jellyfin.Data.Entities.User)