< 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: 417
Uncovered lines: 652
Coverable lines: 1069
Total lines: 3258
Line coverage: 39%
Branch coverage
31%
Covered branches: 190
Total branches: 598
Branch coverage: 31.7%
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%191055.55%
DeleteItem(...)100%210%
DeleteItem(...)0%620%
DeleteItem(...)23.91%9394625%
GetMetadataPaths(...)50%2275%
GetInternalMetadataPaths(...)12.5%20842.85%
ResolveItem(...)100%44100%
Resolve(...)100%1140%
GetNewItemId(...)100%11100%
GetNewItemIdInternal(...)100%66100%
ResolvePath(...)100%22100%
ResolvePath(...)70%272074.19%
IgnoreFile(...)100%11100%
NormalizeRootPathList(...)50%2291.66%
ShouldResolvePathContents(...)100%11100%
ResolvePaths(...)100%11100%
ResolvePaths(...)58.33%221258.33%
CreateRootFolder()71.42%141488%
GetUserRootFolder()80%111079.16%
FindByPath(...)100%210%
GetPerson(...)50%2280%
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%7675%
GetItemById(...)100%22100%
GetItemById(...)50%22100%
GetItemById(...)50%22100%
GetItemList(...)100%1010100%
GetItemList(...)100%11100%
GetCount(...)0%7280%
GetItemList(...)0%4260%
GetLatestItemList(...)0%4260%
GetNextUpSeriesKeys(...)0%4260%
QueryItems(...)0%2040%
GetItemIds(...)50%2266.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%6688.88%
AddUserToQuery(...)100%1616100%
GetTopParentIdsForQuery(...)8.33%4342410.71%
ResolveIntro(...)0%110100%
Sort(...)57.14%341453.33%
Sort(...)0%210140%
GetComparer(...)50%3237.5%
CreateItem(...)100%210%
CreateItems(...)90%101083.33%
ImageNeedsRefresh(...)0%156120%
UpdateItemAsync(...)100%11100%
ReportItemRemoved(...)100%2276.92%
RetrieveItem(...)100%11100%
GetCollectionFolders(...)100%11100%
GetCollectionFolders(...)80%111081.81%
GetCollectionFoldersInternal(...)100%11100%
GetLibraryOptions(...)75%44100%
GetContentType(...)50%4471.42%
GetInheritedContentType(...)50%2283.33%
GetConfiguredContentType(...)100%210%
GetConfiguredContentType(...)100%210%
GetConfiguredContentType(...)50%2266.66%
GetContentTypeOverride(...)50%4485.71%
GetTopFolderContentType(...)50%11863.63%
GetNamedView(...)100%210%
GetNamedView(...)0%4260%
GetNamedView(...)0%420200%
GetShadowView(...)0%272160%
GetNamedView(...)0%600240%
GetParentItem(...)33.33%8660%
QueueLibraryScan()100%210%
GetSeasonNumberFromPath(...)0%2040%
FillMissingEpisodeNumbersFromPath(...)0%4422660%
ParseName(...)0%620%
GetPathAfterNetworkSubstitution(...)25%6450%
GetPeople(...)100%210%
GetPeople(...)50%11425%
GetPeopleItems(...)100%210%
GetPeopleNames(...)100%210%
UpdatePeople(...)100%210%
StartScanInBackground()100%11100%
AddMediaPath(...)100%1150%
AddMediaPathInternal(...)25%36823.8%
UpdateMediaPath(...)100%210%
SyncLibraryOptionsToLocations(...)0%7280%
RemoveContentTypeOverrides(...)0%210140%
RemoveMediaPath(...)25%9433.33%
ItemIsVisible(...)16.66%14640%

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.Generic;
 6using System.Globalization;
 7using System.IO;
 8using System.Linq;
 9using System.Net;
 10using System.Net.Http;
 11using System.Threading;
 12using System.Threading.Tasks;
 13using BitFaster.Caching.Lru;
 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;
 22using Jellyfin.Data.Enums;
 23using Jellyfin.Database.Implementations.Entities;
 24using Jellyfin.Database.Implementations.Enums;
 25using Jellyfin.Extensions;
 26using MediaBrowser.Common.Extensions;
 27using MediaBrowser.Controller;
 28using MediaBrowser.Controller.Configuration;
 29using MediaBrowser.Controller.Drawing;
 30using MediaBrowser.Controller.Dto;
 31using MediaBrowser.Controller.Entities;
 32using MediaBrowser.Controller.Entities.Audio;
 33using MediaBrowser.Controller.IO;
 34using MediaBrowser.Controller.Library;
 35using MediaBrowser.Controller.LiveTv;
 36using MediaBrowser.Controller.MediaEncoding;
 37using MediaBrowser.Controller.Persistence;
 38using MediaBrowser.Controller.Providers;
 39using MediaBrowser.Controller.Resolvers;
 40using MediaBrowser.Controller.Sorting;
 41using MediaBrowser.Model.Configuration;
 42using MediaBrowser.Model.Dlna;
 43using MediaBrowser.Model.Drawing;
 44using MediaBrowser.Model.Dto;
 45using MediaBrowser.Model.Entities;
 46using MediaBrowser.Model.IO;
 47using MediaBrowser.Model.Library;
 48using MediaBrowser.Model.Querying;
 49using MediaBrowser.Model.Tasks;
 50using Microsoft.Extensions.Logging;
 51using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 52using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
 53using Genre = MediaBrowser.Controller.Entities.Genre;
 54using Person = MediaBrowser.Controller.Entities.Person;
 55using VideoResolver = Emby.Naming.Video.VideoResolver;
 56
 57namespace Emby.Server.Implementations.Library
 58{
 59    /// <summary>
 60    /// Class LibraryManager.
 61    /// </summary>
 62    public class LibraryManager : ILibraryManager
 63    {
 64        private const string ShortcutFileExtension = ".mblink";
 65
 66        private readonly ILogger<LibraryManager> _logger;
 67        private readonly ITaskManager _taskManager;
 68        private readonly IUserManager _userManager;
 69        private readonly IUserDataManager _userDataRepository;
 70        private readonly IServerConfigurationManager _configurationManager;
 71        private readonly Lazy<ILibraryMonitor> _libraryMonitorFactory;
 72        private readonly Lazy<IProviderManager> _providerManagerFactory;
 73        private readonly Lazy<IUserViewManager> _userviewManagerFactory;
 74        private readonly IServerApplicationHost _appHost;
 75        private readonly IMediaEncoder _mediaEncoder;
 76        private readonly IFileSystem _fileSystem;
 77        private readonly IItemRepository _itemRepository;
 78        private readonly IImageProcessor _imageProcessor;
 79        private readonly NamingOptions _namingOptions;
 80        private readonly IPeopleRepository _peopleRepository;
 81        private readonly ExtraResolver _extraResolver;
 82        private readonly IPathManager _pathManager;
 83        private readonly FastConcurrentLru<Guid, BaseItem> _cache;
 84
 85        /// <summary>
 86        /// The _root folder sync lock.
 87        /// </summary>
 2888        private readonly Lock _rootFolderSyncLock = new();
 2889        private readonly Lock _userRootFolderSyncLock = new();
 90
 2891        private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
 92
 93        /// <summary>
 94        /// The _root folder.
 95        /// </summary>
 96        private volatile AggregateFolder? _rootFolder;
 97        private volatile UserRootFolder? _userRootFolder;
 98
 99        private bool _wizardCompleted;
 100
 101        /// <summary>
 102        /// Initializes a new instance of the <see cref="LibraryManager" /> class.
 103        /// </summary>
 104        /// <param name="appHost">The application host.</param>
 105        /// <param name="loggerFactory">The logger factory.</param>
 106        /// <param name="taskManager">The task manager.</param>
 107        /// <param name="userManager">The user manager.</param>
 108        /// <param name="configurationManager">The configuration manager.</param>
 109        /// <param name="userDataRepository">The user data repository.</param>
 110        /// <param name="libraryMonitorFactory">The library monitor.</param>
 111        /// <param name="fileSystem">The file system.</param>
 112        /// <param name="providerManagerFactory">The provider manager.</param>
 113        /// <param name="userviewManagerFactory">The userview manager.</param>
 114        /// <param name="mediaEncoder">The media encoder.</param>
 115        /// <param name="itemRepository">The item repository.</param>
 116        /// <param name="imageProcessor">The image processor.</param>
 117        /// <param name="namingOptions">The naming options.</param>
 118        /// <param name="directoryService">The directory service.</param>
 119        /// <param name="peopleRepository">The people repository.</param>
 120        /// <param name="pathManager">The path manager.</param>
 121        public LibraryManager(
 122            IServerApplicationHost appHost,
 123            ILoggerFactory loggerFactory,
 124            ITaskManager taskManager,
 125            IUserManager userManager,
 126            IServerConfigurationManager configurationManager,
 127            IUserDataManager userDataRepository,
 128            Lazy<ILibraryMonitor> libraryMonitorFactory,
 129            IFileSystem fileSystem,
 130            Lazy<IProviderManager> providerManagerFactory,
 131            Lazy<IUserViewManager> userviewManagerFactory,
 132            IMediaEncoder mediaEncoder,
 133            IItemRepository itemRepository,
 134            IImageProcessor imageProcessor,
 135            NamingOptions namingOptions,
 136            IDirectoryService directoryService,
 137            IPeopleRepository peopleRepository,
 138            IPathManager pathManager)
 139        {
 28140            _appHost = appHost;
 28141            _logger = loggerFactory.CreateLogger<LibraryManager>();
 28142            _taskManager = taskManager;
 28143            _userManager = userManager;
 28144            _configurationManager = configurationManager;
 28145            _userDataRepository = userDataRepository;
 28146            _libraryMonitorFactory = libraryMonitorFactory;
 28147            _fileSystem = fileSystem;
 28148            _providerManagerFactory = providerManagerFactory;
 28149            _userviewManagerFactory = userviewManagerFactory;
 28150            _mediaEncoder = mediaEncoder;
 28151            _itemRepository = itemRepository;
 28152            _imageProcessor = imageProcessor;
 153
 28154            _cache = new FastConcurrentLru<Guid, BaseItem>(_configurationManager.Configuration.CacheSize);
 155
 28156            _namingOptions = namingOptions;
 28157            _peopleRepository = peopleRepository;
 28158            _pathManager = pathManager;
 28159            _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryServ
 160
 28161            _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
 162
 28163            RecordConfigurationValues(_configurationManager.Configuration);
 28164        }
 165
 166        /// <summary>
 167        /// Occurs when [item added].
 168        /// </summary>
 169        public event EventHandler<ItemChangeEventArgs>? ItemAdded;
 170
 171        /// <summary>
 172        /// Occurs when [item updated].
 173        /// </summary>
 174        public event EventHandler<ItemChangeEventArgs>? ItemUpdated;
 175
 176        /// <summary>
 177        /// Occurs when [item removed].
 178        /// </summary>
 179        public event EventHandler<ItemChangeEventArgs>? ItemRemoved;
 180
 181        /// <summary>
 182        /// Gets the root folder.
 183        /// </summary>
 184        /// <value>The root folder.</value>
 185        public AggregateFolder RootFolder
 186        {
 187            get
 188            {
 145189                if (_rootFolder is null)
 21190                {
 191                    lock (_rootFolderSyncLock)
 192                    {
 21193                        _rootFolder ??= CreateRootFolder();
 21194                    }
 195                }
 196
 145197                return _rootFolder;
 198            }
 199        }
 200
 41201        private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
 202
 79203        private IProviderManager ProviderManager => _providerManagerFactory.Value;
 204
 1205        private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
 206
 207        /// <summary>
 208        /// Gets or sets the postscan tasks.
 209        /// </summary>
 210        /// <value>The postscan tasks.</value>
 211        private ILibraryPostScanTask[] PostscanTasks { get; set; } = [];
 212
 213        /// <summary>
 214        /// Gets or sets the intro providers.
 215        /// </summary>
 216        /// <value>The intro providers.</value>
 217        private IIntroProvider[] IntroProviders { get; set; } = [];
 218
 219        /// <summary>
 220        /// Gets or sets the list of entity resolution ignore rules.
 221        /// </summary>
 222        /// <value>The entity resolution ignore rules.</value>
 223        private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = [];
 224
 225        /// <summary>
 226        /// Gets or sets the list of currently registered entity resolvers.
 227        /// </summary>
 228        /// <value>The entity resolvers enumerable.</value>
 229        private IItemResolver[] EntityResolvers { get; set; } = [];
 230
 231        private IMultiItemResolver[] MultiItemResolvers { get; set; } = [];
 232
 233        /// <summary>
 234        /// Gets or sets the comparers.
 235        /// </summary>
 236        /// <value>The comparers.</value>
 237        private IBaseItemComparer[] Comparers { get; set; } = [];
 238
 239        public bool IsScanRunning { get; private set; }
 240
 241        /// <summary>
 242        /// Adds the parts.
 243        /// </summary>
 244        /// <param name="rules">The rules.</param>
 245        /// <param name="resolvers">The resolvers.</param>
 246        /// <param name="introProviders">The intro providers.</param>
 247        /// <param name="itemComparers">The item comparers.</param>
 248        /// <param name="postscanTasks">The post scan tasks.</param>
 249        public void AddParts(
 250            IEnumerable<IResolverIgnoreRule> rules,
 251            IEnumerable<IItemResolver> resolvers,
 252            IEnumerable<IIntroProvider> introProviders,
 253            IEnumerable<IBaseItemComparer> itemComparers,
 254            IEnumerable<ILibraryPostScanTask> postscanTasks)
 255        {
 28256            EntityResolutionIgnoreRules = rules.ToArray();
 28257            EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
 28258            MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
 28259            IntroProviders = introProviders.ToArray();
 28260            Comparers = itemComparers.ToArray();
 28261            PostscanTasks = postscanTasks.ToArray();
 28262        }
 263
 264        /// <summary>
 265        /// Records the configuration values.
 266        /// </summary>
 267        /// <param name="configuration">The configuration.</param>
 268        private void RecordConfigurationValues(ServerConfiguration configuration)
 269        {
 66270            _wizardCompleted = configuration.IsStartupWizardCompleted;
 66271        }
 272
 273        /// <summary>
 274        /// Configurations the updated.
 275        /// </summary>
 276        /// <param name="sender">The sender.</param>
 277        /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
 278        private void ConfigurationUpdated(object? sender, EventArgs e)
 279        {
 38280            var config = _configurationManager.Configuration;
 281
 38282            var wizardChanged = config.IsStartupWizardCompleted != _wizardCompleted;
 283
 38284            RecordConfigurationValues(config);
 285
 38286            if (wizardChanged)
 287            {
 16288                _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
 289            }
 38290        }
 291
 292        public void RegisterItem(BaseItem item)
 293        {
 141294            ArgumentNullException.ThrowIfNull(item);
 295
 141296            if (item is IItemByName)
 297            {
 0298                if (item is not MusicArtist)
 299                {
 0300                    return;
 301                }
 302            }
 141303            else if (!item.IsFolder)
 304            {
 0305                if (item is not Video && item is not LiveTvChannel)
 306                {
 0307                    return;
 308                }
 309            }
 310
 141311            _cache.AddOrUpdate(item.Id, item);
 141312        }
 313
 314        public void DeleteItem(BaseItem item, DeleteOptions options)
 315        {
 0316            DeleteItem(item, options, false);
 0317        }
 318
 319        public void DeleteItem(BaseItem item, DeleteOptions options, bool notifyParentItem)
 320        {
 0321            ArgumentNullException.ThrowIfNull(item);
 322
 0323            var parent = item.GetOwner() ?? item.GetParent();
 324
 0325            DeleteItem(item, options, parent, notifyParentItem);
 0326        }
 327
 328        public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
 329        {
 2330            ArgumentNullException.ThrowIfNull(item);
 331
 2332            if (item.SourceType == SourceType.Channel)
 333            {
 0334                if (options.DeleteFromExternalProvider)
 335                {
 336                    try
 337                    {
 0338                        BaseItem.ChannelManager.DeleteItem(item).GetAwaiter().GetResult();
 0339                    }
 0340                    catch (ArgumentException)
 341                    {
 342                        // channel no longer installed
 0343                    }
 344                }
 345
 0346                options.DeleteFileLocation = false;
 347            }
 348
 2349            if (item is LiveTvProgram)
 350            {
 0351                _logger.LogDebug(
 0352                    "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0353                    item.GetType().Name,
 0354                    item.Name ?? "Unknown name",
 0355                    item.Path ?? string.Empty,
 0356                    item.Id);
 357            }
 358            else
 359            {
 2360                _logger.LogInformation(
 2361                    "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 2362                    item.GetType().Name,
 2363                    item.Name ?? "Unknown name",
 2364                    item.Path ?? string.Empty,
 2365                    item.Id);
 366            }
 367
 2368            var children = item.IsFolder
 2369                ? ((Folder)item).GetRecursiveChildren(false)
 2370                : [];
 371
 8372            foreach (var metadataPath in GetMetadataPaths(item, children))
 373            {
 2374                if (!Directory.Exists(metadataPath))
 375                {
 376                    continue;
 377                }
 378
 0379                _logger.LogDebug(
 0380                    "Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0381                    item.GetType().Name,
 0382                    item.Name ?? "Unknown name",
 0383                    metadataPath,
 0384                    item.Id);
 385
 386                try
 387                {
 0388                    Directory.Delete(metadataPath, true);
 0389                }
 0390                catch (Exception ex)
 391                {
 0392                    _logger.LogError(ex, "Error deleting {MetadataPath}", metadataPath);
 0393                }
 394            }
 395
 2396            if (options.DeleteFileLocation && item.IsFileProtocol)
 397            {
 398                // Assume only the first is required
 399                // Add this flag to GetDeletePaths if required in the future
 0400                var isRequiredForDelete = true;
 401
 0402                foreach (var fileSystemInfo in item.GetDeletePaths())
 403                {
 0404                    if (Directory.Exists(fileSystemInfo.FullName) || File.Exists(fileSystemInfo.FullName))
 405                    {
 406                        try
 407                        {
 0408                            _logger.LogInformation(
 0409                                "Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0410                                item.GetType().Name,
 0411                                item.Name ?? "Unknown name",
 0412                                fileSystemInfo.FullName,
 0413                                item.Id);
 414
 0415                            if (fileSystemInfo.IsDirectory)
 416                            {
 0417                                Directory.Delete(fileSystemInfo.FullName, true);
 418                            }
 419                            else
 420                            {
 0421                                File.Delete(fileSystemInfo.FullName);
 422                            }
 0423                        }
 0424                        catch (DirectoryNotFoundException)
 425                        {
 0426                            _logger.LogInformation(
 0427                                "Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Pa
 0428                                item.GetType().Name,
 0429                                item.Name ?? "Unknown name",
 0430                                fileSystemInfo.FullName,
 0431                                item.Id);
 0432                        }
 0433                        catch (FileNotFoundException)
 434                        {
 0435                            _logger.LogInformation(
 0436                                "File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, 
 0437                                item.GetType().Name,
 0438                                item.Name ?? "Unknown name",
 0439                                fileSystemInfo.FullName,
 0440                                item.Id);
 0441                        }
 0442                        catch (IOException)
 443                        {
 0444                            if (isRequiredForDelete)
 445                            {
 0446                                throw;
 447                            }
 0448                        }
 0449                        catch (UnauthorizedAccessException)
 450                        {
 0451                            if (isRequiredForDelete)
 452                            {
 0453                                throw;
 454                            }
 0455                        }
 456                    }
 457
 0458                    isRequiredForDelete = false;
 459                }
 460            }
 461
 2462            item.SetParent(null);
 463
 2464            _itemRepository.DeleteItem(item.Id);
 2465            _cache.TryRemove(item.Id, out _);
 4466            foreach (var child in children)
 467            {
 0468                _itemRepository.DeleteItem(child.Id);
 0469                _cache.TryRemove(child.Id, out _);
 470            }
 471
 2472            ReportItemRemoved(item, parent);
 2473        }
 474
 475        private List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
 476        {
 2477            var list = GetInternalMetadataPaths(item);
 4478            foreach (var child in children)
 479            {
 0480                list.AddRange(GetInternalMetadataPaths(child));
 481            }
 482
 2483            return list;
 484        }
 485
 486        private List<string> GetInternalMetadataPaths(BaseItem item)
 487        {
 2488            var list = new List<string>
 2489            {
 2490                item.GetInternalMetadataPath()
 2491            };
 492
 2493            if (item is Video video)
 494            {
 495                // Trickplay
 0496                list.Add(_pathManager.GetTrickplayDirectory(video));
 497
 498                // Subtitles and attachments
 0499                foreach (var mediaSource in item.GetMediaSources(false))
 500                {
 0501                    var subtitleFolder = _pathManager.GetSubtitleFolderPath(mediaSource.Id);
 0502                    if (subtitleFolder is not null)
 503                    {
 0504                        list.Add(subtitleFolder);
 505                    }
 506
 0507                    var attachmentFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
 0508                    if (attachmentFolder is not null)
 509                    {
 0510                        list.Add(attachmentFolder);
 511                    }
 512                }
 513            }
 514
 2515            return list;
 516        }
 517
 518        /// <summary>
 519        /// Resolves the item.
 520        /// </summary>
 521        /// <param name="args">The args.</param>
 522        /// <param name="resolvers">The resolvers.</param>
 523        /// <returns>BaseItem.</returns>
 524        private BaseItem? ResolveItem(ItemResolveArgs args, IItemResolver[]? resolvers)
 525        {
 74526            var item = (resolvers ?? EntityResolvers).Select(r => Resolve(args, r))
 74527                .FirstOrDefault(i => i is not null);
 528
 74529            if (item is not null)
 530            {
 66531                ResolverHelper.SetInitialItemValues(item, args, _fileSystem, this);
 532            }
 533
 74534            return item;
 535        }
 536
 537        private BaseItem? Resolve(ItemResolveArgs args, IItemResolver resolver)
 538        {
 539            try
 540            {
 276541                return resolver.ResolvePath(args);
 542            }
 0543            catch (Exception ex)
 544            {
 0545                _logger.LogError(ex, "Error in {Resolver} resolving {Path}", resolver.GetType().Name, args.Path);
 0546                return null;
 547            }
 276548        }
 549
 550        public Guid GetNewItemId(string key, Type type)
 551        {
 129552            return GetNewItemIdInternal(key, type, false);
 553        }
 554
 555        private Guid GetNewItemIdInternal(string key, Type type, bool forceCaseInsensitive)
 556        {
 130557            ArgumentException.ThrowIfNullOrEmpty(key);
 130558            ArgumentNullException.ThrowIfNull(type);
 559
 130560            string programDataPath = _configurationManager.ApplicationPaths.ProgramDataPath;
 130561            if (key.StartsWith(programDataPath, StringComparison.Ordinal))
 562            {
 563                // Try to normalize paths located underneath program-data in an attempt to make them more portable
 113564                key = key.Substring(programDataPath.Length)
 113565                    .TrimStart('/', '\\')
 113566                    .Replace('/', '\\');
 567            }
 568
 130569            if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
 570            {
 1571                key = key.ToLowerInvariant();
 572            }
 573
 130574            key = type.FullName + key;
 575
 130576            return key.GetMD5();
 577        }
 578
 579        public BaseItem? ResolvePath(FileSystemMetadata fileInfo, Folder? parent = null, IDirectoryService? directorySer
 42580            => ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent);
 581
 582        private BaseItem? ResolvePath(
 583            FileSystemMetadata fileInfo,
 584            IDirectoryService directoryService,
 585            IItemResolver[]? resolvers,
 586            Folder? parent = null,
 587            CollectionType? collectionType = null,
 588            LibraryOptions? libraryOptions = null)
 589        {
 74590            ArgumentNullException.ThrowIfNull(fileInfo);
 591
 74592            var fullPath = fileInfo.FullName;
 593
 74594            if (collectionType is null && parent is not null)
 595            {
 15596                collectionType = GetContentTypeOverride(fullPath, true);
 597            }
 598
 74599            var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, this)
 74600            {
 74601                Parent = parent,
 74602                FileInfo = fileInfo,
 74603                CollectionType = collectionType,
 74604                LibraryOptions = libraryOptions
 74605            };
 606
 607            // Return null if ignore rules deem that we should do so
 74608            if (IgnoreFile(args.FileInfo, args.Parent))
 609            {
 0610                return null;
 611            }
 612
 613            // Gather child folder and files
 74614            if (args.IsDirectory)
 615            {
 49616                var isPhysicalRoot = args.IsPhysicalRoot;
 617
 618                // When resolving the root, we need it's grandchildren (children of user views)
 49619                var flattenFolderDepth = isPhysicalRoot ? 2 : 0;
 620
 621                FileSystemMetadata[] files;
 49622                var isVf = args.IsVf;
 623
 624                try
 625                {
 49626                    files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, _fileSystem, _appHost, _l
 49627                }
 0628                catch (Exception ex)
 629                {
 0630                    if (parent is not null && parent.IsPhysicalRoot)
 631                    {
 0632                        _logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPh
 633
 0634                        files = [];
 635                    }
 636                    else
 637                    {
 0638                        throw;
 639                    }
 0640                }
 641
 642                // Need to remove subpaths that may have been resolved from shortcuts
 643                // Example: if \\server\movies exists, then strip out \\server\movies\action
 49644                if (isPhysicalRoot)
 645                {
 21646                    files = NormalizeRootPathList(files).ToArray();
 647                }
 648
 49649                args.FileSystemChildren = files;
 650            }
 651
 652            // Check to see if we should resolve based on our contents
 74653            if (args.IsDirectory && !ShouldResolvePathContents(args))
 654            {
 0655                return null;
 656            }
 657
 74658            return ResolveItem(args, resolvers);
 659        }
 660
 661        public bool IgnoreFile(FileSystemMetadata file, BaseItem? parent)
 89662            => EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(file, parent));
 663
 664        public List<FileSystemMetadata> NormalizeRootPathList(IEnumerable<FileSystemMetadata> paths)
 665        {
 69666            var originalList = paths.ToList();
 667
 69668            var list = originalList.Where(i => i.IsDirectory)
 69669                .Select(i => Path.TrimEndingDirectorySeparator(i.FullName))
 69670                .Distinct(StringComparer.OrdinalIgnoreCase)
 69671                .ToList();
 672
 69673            var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.OrdinalIgnoreCase) && list.Any(i
 69674                .ToList();
 675
 138676            foreach (var dupe in dupes)
 677            {
 0678                _logger.LogInformation("Found duplicate path: {0}", dupe);
 679            }
 680
 69681            var newList = list.Except(dupes, StringComparer.OrdinalIgnoreCase).Select(_fileSystem.GetDirectoryInfo).ToLi
 69682            newList.AddRange(originalList.Where(i => !i.IsDirectory));
 69683            return newList;
 684        }
 685
 686        /// <summary>
 687        /// Determines whether a path should be ignored based on its contents - called after the contents have been read
 688        /// </summary>
 689        /// <param name="args">The args.</param>
 690        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
 691        private static bool ShouldResolvePathContents(ItemResolveArgs args)
 692        {
 693            // Ignore any folders containing a file called .ignore
 49694            return !args.ContainsFileSystemEntryByName(".ignore");
 695        }
 696
 697        public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryServ
 698        {
 42699            return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers);
 700        }
 701
 702        public IEnumerable<BaseItem> ResolvePaths(
 703            IEnumerable<FileSystemMetadata> files,
 704            IDirectoryService directoryService,
 705            Folder parent,
 706            LibraryOptions libraryOptions,
 707            CollectionType? collectionType,
 708            IItemResolver[] resolvers)
 709        {
 42710            var fileList = files.Where(i => !IgnoreFile(i, parent)).ToList();
 711
 42712            if (parent is not null)
 713            {
 42714                var multiItemResolvers = resolvers is null ? MultiItemResolvers : resolvers.OfType<IMultiItemResolver>()
 715
 252716                foreach (var resolver in multiItemResolvers)
 717                {
 84718                    var result = resolver.ResolveMultiple(parent, fileList, collectionType, directoryService);
 719
 84720                    if (result?.Items.Count > 0)
 721                    {
 0722                        var items = result.Items;
 0723                        items.RemoveAll(item => !ResolverHelper.SetInitialItemValues(item, parent, this, directoryServic
 0724                        items.AddRange(ResolveFileList(result.ExtraFiles, directoryService, parent, collectionType, reso
 0725                        return items;
 726                    }
 727                }
 728            }
 729
 42730            return ResolveFileList(fileList, directoryService, parent, collectionType, resolvers, libraryOptions);
 0731        }
 732
 733        private IEnumerable<BaseItem> ResolveFileList(
 734            IReadOnlyList<FileSystemMetadata> fileList,
 735            IDirectoryService directoryService,
 736            Folder? parent,
 737            CollectionType? collectionType,
 738            IItemResolver[]? resolvers,
 739            LibraryOptions libraryOptions)
 740        {
 741            // Given that fileList is a list we can save enumerator allocations by indexing
 742            for (var i = 0; i < fileList.Count; i++)
 743            {
 744                var file = fileList[i];
 745                BaseItem? result = null;
 746                try
 747                {
 748                    result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions);
 749                }
 750                catch (Exception ex)
 751                {
 752                    _logger.LogError(ex, "Error resolving path {Path}", file.FullName);
 753                }
 754
 755                if (result is not null)
 756                {
 757                    yield return result;
 758                }
 759            }
 760        }
 761
 762        /// <summary>
 763        /// Creates the root media folder.
 764        /// </summary>
 765        /// <returns>AggregateFolder.</returns>
 766        /// <exception cref="InvalidOperationException">Cannot create the root folder until plugins have loaded.</except
 767        public AggregateFolder CreateRootFolder()
 768        {
 21769            var rootFolderPath = _configurationManager.ApplicationPaths.RootFolderPath;
 770
 21771            Directory.CreateDirectory(rootFolderPath);
 772
 21773            var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ??
 21774                             (ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)) as Folder ?? throw new InvalidOp
 21775                             .DeepCopy<Folder, AggregateFolder>();
 776
 777            // In case program data folder was moved
 21778            if (!string.Equals(rootFolder.Path, rootFolderPath, StringComparison.Ordinal))
 779            {
 0780                _logger.LogInformation("Resetting root folder path to {0}", rootFolderPath);
 0781                rootFolder.Path = rootFolderPath;
 782            }
 783
 784            // Add in the plug-in folders
 21785            var path = Path.Combine(_configurationManager.ApplicationPaths.DataPath, "playlists");
 786
 21787            Directory.CreateDirectory(path);
 788
 21789            Folder folder = new PlaylistsFolder
 21790            {
 21791                Path = path
 21792            };
 793
 21794            if (folder.Id.IsEmpty())
 795            {
 21796                folder.Id = GetNewItemId(folder.Path, folder.GetType());
 797            }
 798
 21799            var dbItem = GetItemById(folder.Id) as BasePluginFolder;
 800
 21801            if (dbItem is not null && string.Equals(dbItem.Path, folder.Path, StringComparison.OrdinalIgnoreCase))
 802            {
 0803                folder = dbItem;
 804            }
 805
 21806            if (!folder.ParentId.Equals(rootFolder.Id))
 807            {
 21808                folder.ParentId = rootFolder.Id;
 21809                folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetRe
 810            }
 811
 21812            rootFolder.AddVirtualChild(folder);
 813
 21814            RegisterItem(folder);
 815
 21816            return rootFolder;
 817        }
 818
 819        public Folder GetUserRootFolder()
 820        {
 758821            if (_userRootFolder is null)
 21822            {
 823                lock (_userRootFolderSyncLock)
 824                {
 21825                    if (_userRootFolder is null)
 826                    {
 21827                        var userRootPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 828
 21829                        _logger.LogDebug("Creating userRootPath at {Path}", userRootPath);
 21830                        Directory.CreateDirectory(userRootPath);
 831
 21832                        var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder));
 21833                        UserRootFolder? tmpItem = null;
 834                        try
 835                        {
 21836                            tmpItem = GetItemById(newItemId) as UserRootFolder;
 21837                        }
 0838                        catch (Exception ex)
 839                        {
 0840                            _logger.LogError(ex, "Error creating UserRootFolder {Path}", newItemId);
 0841                        }
 842
 21843                        if (tmpItem is null)
 844                        {
 21845                            _logger.LogDebug("Creating new userRootFolder with DeepCopy");
 21846                            tmpItem = (ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath)) as Folder ?? throw new In
 21847                                        .DeepCopy<Folder, UserRootFolder>();
 848                        }
 849
 850                        // In case program data folder was moved
 21851                        if (!string.Equals(tmpItem.Path, userRootPath, StringComparison.Ordinal))
 852                        {
 0853                            _logger.LogInformation("Resetting user root folder path to {0}", userRootPath);
 0854                            tmpItem.Path = userRootPath;
 855                        }
 856
 21857                        _userRootFolder = tmpItem;
 21858                        _logger.LogDebug("Setting userRootFolder: {Folder}", _userRootFolder);
 859                    }
 21860                }
 861            }
 862
 758863            return _userRootFolder;
 864        }
 865
 866        /// <inheritdoc />
 867        public BaseItem? FindByPath(string path, bool? isFolder)
 868        {
 869            // If this returns multiple items it could be tricky figuring out which one is correct.
 870            // In most cases, the newest one will be and the others obsolete but not yet cleaned up
 0871            ArgumentException.ThrowIfNullOrEmpty(path);
 872
 0873            var query = new InternalItemsQuery
 0874            {
 0875                Path = path,
 0876                IsFolder = isFolder,
 0877                OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) },
 0878                Limit = 1,
 0879                DtoOptions = new DtoOptions(true)
 0880            };
 881
 0882            return GetItemList(query)
 0883                .FirstOrDefault();
 884        }
 885
 886        /// <inheritdoc />
 887        public Person? GetPerson(string name)
 888        {
 1889            var path = Person.GetPath(name);
 1890            var id = GetItemByNameId<Person>(path);
 1891            if (GetItemById(id) is Person item)
 892            {
 0893                return item;
 894            }
 895
 1896            return null;
 897        }
 898
 899        /// <summary>
 900        /// Gets the studio.
 901        /// </summary>
 902        /// <param name="name">The name.</param>
 903        /// <returns>Task{Studio}.</returns>
 904        public Studio GetStudio(string name)
 905        {
 0906            return CreateItemByName<Studio>(Studio.GetPath, name, new DtoOptions(true));
 907        }
 908
 909        public Guid GetStudioId(string name)
 910        {
 0911            return GetItemByNameId<Studio>(Studio.GetPath(name));
 912        }
 913
 914        public Guid GetGenreId(string name)
 915        {
 0916            return GetItemByNameId<Genre>(Genre.GetPath(name));
 917        }
 918
 919        public Guid GetMusicGenreId(string name)
 920        {
 0921            return GetItemByNameId<MusicGenre>(MusicGenre.GetPath(name));
 922        }
 923
 924        /// <summary>
 925        /// Gets the genre.
 926        /// </summary>
 927        /// <param name="name">The name.</param>
 928        /// <returns>Task{Genre}.</returns>
 929        public Genre GetGenre(string name)
 930        {
 0931            return CreateItemByName<Genre>(Genre.GetPath, name, new DtoOptions(true));
 932        }
 933
 934        /// <summary>
 935        /// Gets the music genre.
 936        /// </summary>
 937        /// <param name="name">The name.</param>
 938        /// <returns>Task{MusicGenre}.</returns>
 939        public MusicGenre GetMusicGenre(string name)
 940        {
 0941            return CreateItemByName<MusicGenre>(MusicGenre.GetPath, name, new DtoOptions(true));
 942        }
 943
 944        /// <summary>
 945        /// Gets the year.
 946        /// </summary>
 947        /// <param name="value">The value.</param>
 948        /// <returns>Task{Year}.</returns>
 949        public Year GetYear(int value)
 950        {
 0951            if (value <= 0)
 952            {
 0953                throw new ArgumentOutOfRangeException(nameof(value), "Years less than or equal to 0 are invalid.");
 954            }
 955
 0956            var name = value.ToString(CultureInfo.InvariantCulture);
 957
 0958            return CreateItemByName<Year>(Year.GetPath, name, new DtoOptions(true));
 959        }
 960
 961        /// <summary>
 962        /// Gets a Genre.
 963        /// </summary>
 964        /// <param name="name">The name.</param>
 965        /// <returns>Task{Genre}.</returns>
 966        public MusicArtist GetArtist(string name)
 967        {
 0968            return GetArtist(name, new DtoOptions(true));
 969        }
 970
 971        public MusicArtist GetArtist(string name, DtoOptions options)
 972        {
 0973            return CreateItemByName<MusicArtist>(MusicArtist.GetPath, name, options);
 974        }
 975
 976        private T CreateItemByName<T>(Func<string, string> getPathFn, string name, DtoOptions options)
 977            where T : BaseItem, new()
 978        {
 0979            if (typeof(T) == typeof(MusicArtist))
 980            {
 0981                var existing = GetItemList(new InternalItemsQuery
 0982                {
 0983                    IncludeItemTypes = new[] { BaseItemKind.MusicArtist },
 0984                    Name = name,
 0985                    DtoOptions = options
 0986                }).Cast<MusicArtist>()
 0987                .OrderBy(i => i.IsAccessedByName ? 1 : 0)
 0988                .Cast<T>()
 0989                .FirstOrDefault();
 990
 0991                if (existing is not null)
 992                {
 0993                    return existing;
 994                }
 995            }
 996
 0997            var path = getPathFn(name);
 0998            var id = GetItemByNameId<T>(path);
 0999            var item = GetItemById(id) as T;
 01000            if (item is null)
 1001            {
 01002                item = new T
 01003                {
 01004                    Name = name,
 01005                    Id = id,
 01006                    DateCreated = DateTime.UtcNow,
 01007                    DateModified = DateTime.UtcNow,
 01008                    Path = path
 01009                };
 1010
 01011                CreateItem(item, null);
 1012            }
 1013
 01014            return item;
 1015        }
 1016
 1017        private Guid GetItemByNameId<T>(string path)
 1018              where T : BaseItem, new()
 1019        {
 11020            var forceCaseInsensitiveId = _configurationManager.Configuration.EnableNormalizedItemByNameIds;
 11021            return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId);
 1022        }
 1023
 1024        /// <inheritdoc />
 1025        public Task ValidatePeopleAsync(IProgress<double> progress, CancellationToken cancellationToken)
 1026        {
 1027            // Ensure the location is available.
 01028            Directory.CreateDirectory(_configurationManager.ApplicationPaths.PeoplePath);
 1029
 01030            return new PeopleValidator(this, _logger, _fileSystem).ValidatePeople(cancellationToken, progress);
 1031        }
 1032
 1033        /// <summary>
 1034        /// Reloads the root media folder.
 1035        /// </summary>
 1036        /// <param name="progress">The progress.</param>
 1037        /// <param name="cancellationToken">The cancellation token.</param>
 1038        /// <returns>Task.</returns>
 1039        public Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken)
 1040        {
 1041            // Just run the scheduled task so that the user can see it
 31042            _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
 1043
 31044            return Task.CompletedTask;
 1045        }
 1046
 1047        /// <summary>
 1048        /// Validates the media library internal.
 1049        /// </summary>
 1050        /// <param name="progress">The progress.</param>
 1051        /// <param name="cancellationToken">The cancellation token.</param>
 1052        /// <returns>Task.</returns>
 1053        public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken)
 1054        {
 1055            IsScanRunning = true;
 1056            LibraryMonitor.Stop();
 1057
 1058            try
 1059            {
 1060                await PerformLibraryValidation(progress, cancellationToken).ConfigureAwait(false);
 1061            }
 1062            finally
 1063            {
 1064                LibraryMonitor.Start();
 1065                IsScanRunning = false;
 1066            }
 1067        }
 1068
 1069        public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
 1070        {
 1071            await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1072
 1073            // Start by just validating the children of the root, but go no further
 1074            await RootFolder.ValidateChildren(
 1075                new Progress<double>(),
 1076                new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
 1077                recursive: false,
 1078                allowRemoveRoot: removeRoot,
 1079                cancellationToken: cancellationToken).ConfigureAwait(false);
 1080
 1081            await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1082
 1083            await GetUserRootFolder().ValidateChildren(
 1084                new Progress<double>(),
 1085                new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
 1086                recursive: false,
 1087                allowRemoveRoot: removeRoot,
 1088                cancellationToken: cancellationToken).ConfigureAwait(false);
 1089
 1090            // Quickly scan CollectionFolders for changes
 1091            foreach (var child in GetUserRootFolder().Children.OfType<Folder>())
 1092            {
 1093                // If the user has somehow deleted the collection directory, remove the metadata from the database.
 1094                if (child is CollectionFolder collectionFolder && !Directory.Exists(collectionFolder.Path))
 1095                {
 1096                    _itemRepository.DeleteItem(collectionFolder.Id);
 1097                }
 1098                else
 1099                {
 1100                    await child.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1101                }
 1102            }
 1103        }
 1104
 1105        private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken)
 1106        {
 1107            _logger.LogInformation("Validating media library");
 1108
 1109            await ValidateTopLibraryFolders(cancellationToken).ConfigureAwait(false);
 1110
 1111            var innerProgress = new Progress<double>(pct => progress.Report(pct * 0.96));
 1112
 1113            // Validate the entire media library
 1114            await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem
 1115
 1116            progress.Report(96);
 1117
 1118            innerProgress = new Progress<double>(pct => progress.Report(96 + (pct * .04)));
 1119
 1120            await RunPostScanTasks(innerProgress, cancellationToken).ConfigureAwait(false);
 1121
 1122            progress.Report(100);
 1123        }
 1124
 1125        /// <summary>
 1126        /// Runs the post scan tasks.
 1127        /// </summary>
 1128        /// <param name="progress">The progress.</param>
 1129        /// <param name="cancellationToken">The cancellation token.</param>
 1130        /// <returns>Task.</returns>
 1131        private async Task RunPostScanTasks(IProgress<double> progress, CancellationToken cancellationToken)
 1132        {
 1133            var tasks = PostscanTasks.ToList();
 1134
 1135            var numComplete = 0;
 1136            var numTasks = tasks.Count;
 1137
 1138            foreach (var task in tasks)
 1139            {
 1140                // Prevent access to modified closure
 1141                var currentNumComplete = numComplete;
 1142
 1143                var innerProgress = new Progress<double>(pct =>
 1144                {
 1145                    double innerPercent = pct;
 1146                    innerPercent /= 100;
 1147                    innerPercent += currentNumComplete;
 1148
 1149                    innerPercent /= numTasks;
 1150                    innerPercent *= 100;
 1151
 1152                    progress.Report(innerPercent);
 1153                });
 1154
 1155                _logger.LogDebug("Running post-scan task {0}", task.GetType().Name);
 1156
 1157                try
 1158                {
 1159                    await task.Run(innerProgress, cancellationToken).ConfigureAwait(false);
 1160                }
 1161                catch (OperationCanceledException)
 1162                {
 1163                    _logger.LogInformation("Post-scan task cancelled: {0}", task.GetType().Name);
 1164                    throw;
 1165                }
 1166                catch (Exception ex)
 1167                {
 1168                    _logger.LogError(ex, "Error running post-scan task");
 1169                }
 1170
 1171                numComplete++;
 1172                double percent = numComplete;
 1173                percent /= numTasks;
 1174                progress.Report(percent * 100);
 1175            }
 1176
 1177            _itemRepository.UpdateInheritedValues();
 1178
 1179            progress.Report(100);
 1180        }
 1181
 1182        /// <summary>
 1183        /// Gets the default view.
 1184        /// </summary>
 1185        /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
 1186        public List<VirtualFolderInfo> GetVirtualFolders()
 1187        {
 231188            return GetVirtualFolders(false);
 1189        }
 1190
 1191        public List<VirtualFolderInfo> GetVirtualFolders(bool includeRefreshState)
 1192        {
 241193            _logger.LogDebug("Getting topLibraryFolders");
 241194            var topLibraryFolders = GetUserRootFolder().Children.ToList();
 1195
 241196            _logger.LogDebug("Getting refreshQueue");
 241197            var refreshQueue = includeRefreshState ? ProviderManager.GetRefreshQueue() : null;
 1198
 241199            return _fileSystem.GetDirectoryPaths(_configurationManager.ApplicationPaths.DefaultUserViewsPath)
 241200                .Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders, refreshQueue))
 241201                .ToList();
 1202        }
 1203
 1204        private VirtualFolderInfo GetVirtualFolderInfo(string dir, List<BaseItem> allCollectionFolders, HashSet<Guid>? r
 1205        {
 11206            var info = new VirtualFolderInfo
 11207            {
 11208                Name = Path.GetFileName(dir),
 11209
 11210                Locations = _fileSystem.GetFilePaths(dir, false)
 11211                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCa
 11212                    .Select(i =>
 11213                    {
 11214                        try
 11215                        {
 11216                            return _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(i));
 11217                        }
 11218                        catch (Exception ex)
 11219                        {
 11220                            _logger.LogError(ex, "Error resolving shortcut file {File}", i);
 11221                            return null;
 11222                        }
 11223                    })
 11224                    .Where(i => i is not null)
 11225                    .Order()
 11226                    .ToArray(),
 11227
 11228                CollectionType = GetCollectionType(dir)
 11229            };
 1230
 11231            var libraryFolder = allCollectionFolders.FirstOrDefault(i => string.Equals(i.Path, dir, StringComparison.Ord
 11232            if (libraryFolder is not null)
 1233            {
 11234                var libraryFolderId = libraryFolder.Id.ToString("N", CultureInfo.InvariantCulture);
 11235                info.ItemId = libraryFolderId;
 11236                if (libraryFolder.HasImage(ImageType.Primary))
 1237                {
 01238                    info.PrimaryImageItemId = libraryFolderId;
 1239                }
 1240
 11241                info.LibraryOptions = GetLibraryOptions(libraryFolder);
 1242
 11243                if (refreshQueue is not null)
 1244                {
 11245                    info.RefreshProgress = libraryFolder.GetRefreshProgress();
 1246
 11247                    info.RefreshStatus = info.RefreshProgress.HasValue ? "Active" : refreshQueue.Contains(libraryFolder.
 1248                }
 1249            }
 1250
 11251            return info;
 1252        }
 1253
 1254        private CollectionTypeOptions? GetCollectionType(string path)
 1255        {
 11256            var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false);
 21257            foreach (ReadOnlySpan<char> file in files)
 1258            {
 01259                if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
 1260                {
 01261                    return res;
 1262                }
 1263            }
 1264
 11265            return null;
 01266        }
 1267
 1268        /// <inheritdoc />
 1269        public BaseItem? GetItemById(Guid id)
 1270        {
 7451271            if (id.IsEmpty())
 1272            {
 01273                throw new ArgumentException("Guid can't be empty", nameof(id));
 1274            }
 1275
 7451276            if (_cache.TryGet(id, out var item))
 1277            {
 5121278                return item;
 1279            }
 1280
 2331281            item = RetrieveItem(id);
 1282
 2331283            if (item is not null)
 1284            {
 01285                RegisterItem(item);
 1286            }
 1287
 2331288            return item;
 1289        }
 1290
 1291        /// <inheritdoc />
 1292        public T? GetItemById<T>(Guid id)
 1293            where T : BaseItem
 1294        {
 231295            var item = GetItemById(id);
 231296            if (item is T typedItem)
 1297            {
 11298                return typedItem;
 1299            }
 1300
 221301            return null;
 1302        }
 1303
 1304        /// <inheritdoc />
 1305        public T? GetItemById<T>(Guid id, Guid userId)
 1306            where T : BaseItem
 1307        {
 11308            var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
 11309            return GetItemById<T>(id, user);
 1310        }
 1311
 1312        /// <inheritdoc />
 1313        public T? GetItemById<T>(Guid id, User? user)
 1314            where T : BaseItem
 1315        {
 211316            var item = GetItemById<T>(id);
 211317            return ItemIsVisible(item, user) ? item : null;
 1318        }
 1319
 1320        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
 1321        {
 751322            if (query.Recursive && !query.ParentId.IsEmpty())
 1323            {
 391324                var parent = GetItemById(query.ParentId);
 391325                if (parent is not null)
 1326                {
 391327                    SetTopParentIdsOrAncestors(query, new[] { parent });
 1328                }
 1329            }
 1330
 751331            if (query.User is not null)
 1332            {
 11333                AddUserToQuery(query, query.User, allowExternalContent);
 1334            }
 1335
 751336            var itemList = _itemRepository.GetItemList(query);
 751337            var user = query.User;
 751338            if (user is not null)
 1339            {
 11340                return itemList.Where(i => i.IsVisible(user)).ToList();
 1341            }
 1342
 741343            return itemList;
 1344        }
 1345
 1346        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
 1347        {
 751348            return GetItemList(query, true);
 1349        }
 1350
 1351        public int GetCount(InternalItemsQuery query)
 1352        {
 01353            if (query.Recursive && !query.ParentId.IsEmpty())
 1354            {
 01355                var parent = GetItemById(query.ParentId);
 01356                if (parent is not null)
 1357                {
 01358                    SetTopParentIdsOrAncestors(query, new[] { parent });
 1359                }
 1360            }
 1361
 01362            if (query.User is not null)
 1363            {
 01364                AddUserToQuery(query, query.User);
 1365            }
 1366
 01367            return _itemRepository.GetCount(query);
 1368        }
 1369
 1370        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
 1371        {
 01372            SetTopParentIdsOrAncestors(query, parents);
 1373
 01374            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1375            {
 01376                if (query.User is not null)
 1377                {
 01378                    AddUserToQuery(query, query.User);
 1379                }
 1380            }
 1381
 01382            return _itemRepository.GetItemList(query);
 1383        }
 1384
 1385        public IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery query, IReadOnlyList<BaseItem> parents, Coll
 1386        {
 01387            SetTopParentIdsOrAncestors(query, parents);
 1388
 01389            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1390            {
 01391                if (query.User is not null)
 1392                {
 01393                    AddUserToQuery(query, query.User);
 1394                }
 1395            }
 1396
 01397            return _itemRepository.GetLatestItemList(query, collectionType);
 1398        }
 1399
 1400        public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents
 1401        {
 01402            SetTopParentIdsOrAncestors(query, parents);
 1403
 01404            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1405            {
 01406                if (query.User is not null)
 1407                {
 01408                    AddUserToQuery(query, query.User);
 1409                }
 1410            }
 1411
 01412            return _itemRepository.GetNextUpSeriesKeys(query, dateCutoff);
 1413        }
 1414
 1415        public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
 1416        {
 01417            if (query.User is not null)
 1418            {
 01419                AddUserToQuery(query, query.User);
 1420            }
 1421
 01422            if (query.EnableTotalRecordCount)
 1423            {
 01424                return _itemRepository.GetItems(query);
 1425            }
 1426
 01427            return new QueryResult<BaseItem>(
 01428                query.StartIndex,
 01429                null,
 01430                _itemRepository.GetItemList(query));
 1431        }
 1432
 1433        public IReadOnlyList<Guid> GetItemIds(InternalItemsQuery query)
 1434        {
 111435            if (query.User is not null)
 1436            {
 01437                AddUserToQuery(query, query.User);
 1438            }
 1439
 111440            return _itemRepository.GetItemIdsList(query);
 1441        }
 1442
 1443        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query)
 1444        {
 01445            if (query.User is not null)
 1446            {
 01447                AddUserToQuery(query, query.User);
 1448            }
 1449
 01450            SetTopParentOrAncestorIds(query);
 01451            return _itemRepository.GetStudios(query);
 1452        }
 1453
 1454        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query)
 1455        {
 01456            if (query.User is not null)
 1457            {
 01458                AddUserToQuery(query, query.User);
 1459            }
 1460
 01461            SetTopParentOrAncestorIds(query);
 01462            return _itemRepository.GetGenres(query);
 1463        }
 1464
 1465        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query)
 1466        {
 01467            if (query.User is not null)
 1468            {
 01469                AddUserToQuery(query, query.User);
 1470            }
 1471
 01472            SetTopParentOrAncestorIds(query);
 01473            return _itemRepository.GetMusicGenres(query);
 1474        }
 1475
 1476        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query)
 1477        {
 01478            if (query.User is not null)
 1479            {
 01480                AddUserToQuery(query, query.User);
 1481            }
 1482
 01483            SetTopParentOrAncestorIds(query);
 01484            return _itemRepository.GetAllArtists(query);
 1485        }
 1486
 1487        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query)
 1488        {
 01489            if (query.User is not null)
 1490            {
 01491                AddUserToQuery(query, query.User);
 1492            }
 1493
 01494            SetTopParentOrAncestorIds(query);
 01495            return _itemRepository.GetArtists(query);
 1496        }
 1497
 1498        private void SetTopParentOrAncestorIds(InternalItemsQuery query)
 1499        {
 01500            var ancestorIds = query.AncestorIds;
 01501            int len = ancestorIds.Length;
 01502            if (len == 0)
 1503            {
 01504                return;
 1505            }
 1506
 01507            var parents = new BaseItem[len];
 01508            for (int i = 0; i < len; i++)
 1509            {
 01510                parents[i] = GetItemById(ancestorIds[i]) ?? throw new ArgumentException($"Failed to find parent with id:
 01511                if (parents[i] is not (ICollectionFolder or UserView))
 1512                {
 01513                    return;
 1514                }
 1515            }
 1516
 1517            // Optimize by querying against top level views
 01518            query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
 01519            query.AncestorIds = [];
 1520
 1521            // Prevent searching in all libraries due to empty filter
 01522            if (query.TopParentIds.Length == 0)
 1523            {
 01524                query.TopParentIds = [Guid.NewGuid()];
 1525            }
 01526        }
 1527
 1528        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
 1529        {
 01530            if (query.User is not null)
 1531            {
 01532                AddUserToQuery(query, query.User);
 1533            }
 1534
 01535            SetTopParentOrAncestorIds(query);
 01536            return _itemRepository.GetAlbumArtists(query);
 1537        }
 1538
 1539        public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query)
 1540        {
 141541            if (query.Recursive && !query.ParentId.IsEmpty())
 1542            {
 131543                var parent = GetItemById(query.ParentId);
 131544                if (parent is not null)
 1545                {
 131546                    SetTopParentIdsOrAncestors(query, new[] { parent });
 1547                }
 1548            }
 1549
 141550            if (query.User is not null)
 1551            {
 11552                AddUserToQuery(query, query.User);
 1553            }
 1554
 141555            if (query.EnableTotalRecordCount)
 1556            {
 11557                return _itemRepository.GetItems(query);
 1558            }
 1559
 131560            return new QueryResult<BaseItem>(
 131561                query.StartIndex,
 131562                null,
 131563                _itemRepository.GetItemList(query));
 1564        }
 1565
 1566        private void SetTopParentIdsOrAncestors(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents)
 1567        {
 521568            if (parents.All(i => i is ICollectionFolder || i is UserView))
 1569            {
 1570                // Optimize by querying against top level views
 131571                query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
 1572
 1573                // Prevent searching in all libraries due to empty filter
 131574                if (query.TopParentIds.Length == 0)
 1575                {
 131576                    query.TopParentIds = new[] { Guid.NewGuid() };
 1577                }
 1578            }
 1579            else
 1580            {
 1581                // We need to be able to query from any arbitrary ancestor up the tree
 391582                query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).ToArray();
 1583
 1584                // Prevent searching in all libraries due to empty filter
 391585                if (query.AncestorIds.Length == 0)
 1586                {
 01587                    query.AncestorIds = new[] { Guid.NewGuid() };
 1588                }
 1589            }
 1590
 521591            query.Parent = null;
 521592        }
 1593
 1594        private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
 1595        {
 21596            if (query.AncestorIds.Length == 0 &&
 21597                query.ParentId.IsEmpty() &&
 21598                query.ChannelIds.Count == 0 &&
 21599                query.TopParentIds.Length == 0 &&
 21600                string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) &&
 21601                string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) &&
 21602                query.ItemIds.Length == 0)
 1603            {
 11604                var userViews = UserViewManager.GetUserViews(new UserViewQuery
 11605                {
 11606                    User = user,
 11607                    IncludeHidden = true,
 11608                    IncludeExternalContent = allowExternalContent
 11609                });
 1610
 11611                query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).ToArray();
 1612
 1613                // Prevent searching in all libraries due to empty filter
 11614                if (query.TopParentIds.Length == 0)
 1615                {
 11616                    query.TopParentIds = new[] { Guid.NewGuid() };
 1617                }
 1618            }
 21619        }
 1620
 1621        private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User? user)
 1622        {
 131623            if (item is UserView view)
 1624            {
 01625                if (view.ViewType == CollectionType.livetv)
 1626                {
 01627                    return new[] { view.Id };
 1628                }
 1629
 1630                // Translate view into folders
 01631                if (!view.DisplayParentId.IsEmpty())
 1632                {
 01633                    var displayParent = GetItemById(view.DisplayParentId);
 01634                    if (displayParent is not null)
 1635                    {
 01636                        return GetTopParentIdsForQuery(displayParent, user);
 1637                    }
 1638
 01639                    return [];
 1640                }
 1641
 01642                if (!view.ParentId.IsEmpty())
 1643                {
 01644                    var displayParent = GetItemById(view.ParentId);
 01645                    if (displayParent is not null)
 1646                    {
 01647                        return GetTopParentIdsForQuery(displayParent, user);
 1648                    }
 1649
 01650                    return [];
 1651                }
 1652
 1653                // Handle grouping
 01654                if (user is not null && view.ViewType != CollectionType.unknown && UserView.IsEligibleForGrouping(view.V
 01655                    && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0)
 1656                {
 01657                    return GetUserRootFolder()
 01658                        .GetChildren(user, true)
 01659                        .OfType<CollectionFolder>()
 01660                        .Where(i => i.CollectionType is null || i.CollectionType == view.ViewType)
 01661                        .Where(i => user.IsFolderGrouped(i.Id))
 01662                        .SelectMany(i => GetTopParentIdsForQuery(i, user));
 1663                }
 1664
 01665                return [];
 1666            }
 1667
 131668            if (item is CollectionFolder collectionFolder)
 1669            {
 131670                return collectionFolder.PhysicalFolderIds;
 1671            }
 1672
 01673            var topParent = item.GetTopParent();
 01674            if (topParent is not null)
 1675            {
 01676                return new[] { topParent.Id };
 1677            }
 1678
 01679            return [];
 1680        }
 1681
 1682        /// <summary>
 1683        /// Gets the intros.
 1684        /// </summary>
 1685        /// <param name="item">The item.</param>
 1686        /// <param name="user">The user.</param>
 1687        /// <returns>IEnumerable{System.String}.</returns>
 1688        public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
 1689        {
 1690            if (IntroProviders.Length == 0)
 1691            {
 1692                return [];
 1693            }
 1694
 1695            var tasks = IntroProviders
 1696                .Select(i => GetIntros(i, item, user));
 1697
 1698            var items = await Task.WhenAll(tasks).ConfigureAwait(false);
 1699
 1700            return items
 1701                .SelectMany(i => i)
 1702                .Select(ResolveIntro)
 1703                .Where(i => i is not null)!; // null values got filtered out
 1704        }
 1705
 1706        /// <summary>
 1707        /// Gets the intros.
 1708        /// </summary>
 1709        /// <param name="provider">The provider.</param>
 1710        /// <param name="item">The item.</param>
 1711        /// <param name="user">The user.</param>
 1712        /// <returns>Task&lt;IEnumerable&lt;IntroInfo&gt;&gt;.</returns>
 1713        private async Task<IEnumerable<IntroInfo>> GetIntros(IIntroProvider provider, BaseItem item, User user)
 1714        {
 1715            try
 1716            {
 1717                return await provider.GetIntros(item, user).ConfigureAwait(false);
 1718            }
 1719            catch (Exception ex)
 1720            {
 1721                _logger.LogError(ex, "Error getting intros");
 1722
 1723                return [];
 1724            }
 1725        }
 1726
 1727        /// <summary>
 1728        /// Resolves the intro.
 1729        /// </summary>
 1730        /// <param name="info">The info.</param>
 1731        /// <returns>Video.</returns>
 1732        private Video? ResolveIntro(IntroInfo info)
 1733        {
 01734            Video? video = null;
 1735
 01736            if (info.ItemId.HasValue)
 1737            {
 1738                // Get an existing item by Id
 01739                video = GetItemById(info.ItemId.Value) as Video;
 1740
 01741                if (video is null)
 1742                {
 01743                    _logger.LogError("Unable to locate item with Id {ID}.", info.ItemId.Value);
 1744                }
 1745            }
 01746            else if (!string.IsNullOrEmpty(info.Path))
 1747            {
 1748                try
 1749                {
 1750                    // Try to resolve the path into a video
 01751                    video = ResolvePath(_fileSystem.GetFileSystemInfo(info.Path)) as Video;
 1752
 01753                    if (video is null)
 1754                    {
 01755                        _logger.LogError("Intro resolver returned null for {Path}.", info.Path);
 1756                    }
 1757                    else
 1758                    {
 1759                        // Pull the saved db item that will include metadata
 01760                        var dbItem = GetItemById(video.Id) as Video;
 1761
 01762                        if (dbItem is not null)
 1763                        {
 01764                            video = dbItem;
 1765                        }
 1766                        else
 1767                        {
 01768                            return null;
 1769                        }
 1770                    }
 01771                }
 01772                catch (Exception ex)
 1773                {
 01774                    _logger.LogError(ex, "Error resolving path {Path}.", info.Path);
 01775                }
 1776            }
 1777            else
 1778            {
 01779                _logger.LogError("IntroProvider returned an IntroInfo with null Path and ItemId.");
 1780            }
 1781
 01782            return video;
 01783        }
 1784
 1785        /// <inheritdoc />
 1786        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortO
 1787        {
 11788            IOrderedEnumerable<BaseItem>? orderedItems = null;
 1789
 41790            foreach (var orderBy in sortBy.Select(o => GetComparer(o, user)).Where(c => c is not null))
 1791            {
 11792                if (orderBy is RandomComparer)
 1793                {
 01794                    var randomItems = items.ToArray();
 01795                    Random.Shared.Shuffle(randomItems);
 01796                    items = randomItems;
 1797                    // Items are no longer ordered at this point, so set orderedItems back to null
 01798                    orderedItems = null;
 1799                }
 11800                else if (orderedItems is null)
 1801                {
 11802                    orderedItems = sortOrder == SortOrder.Descending
 11803                        ? items.OrderByDescending(i => i, orderBy)
 11804                        : items.OrderBy(i => i, orderBy);
 1805                }
 1806                else
 1807                {
 01808                    orderedItems = sortOrder == SortOrder.Descending
 01809                        ? orderedItems!.ThenByDescending(i => i, orderBy)
 01810                        : orderedItems!.ThenBy(i => i, orderBy); // orderedItems is set during the first iteration
 1811                }
 1812            }
 1813
 11814            return orderedItems ?? items;
 1815        }
 1816
 1817        /// <inheritdoc />
 1818        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<(ItemSortBy OrderBy, Sort
 1819        {
 01820            IOrderedEnumerable<BaseItem>? orderedItems = null;
 1821
 01822            foreach (var (name, sortOrder) in orderBy)
 1823            {
 01824                var comparer = GetComparer(name, user);
 01825                if (comparer is null)
 1826                {
 1827                    continue;
 1828                }
 1829
 01830                if (comparer is RandomComparer)
 1831                {
 01832                    var randomItems = items.ToArray();
 01833                    Random.Shared.Shuffle(randomItems);
 01834                    items = randomItems;
 1835                    // Items are no longer ordered at this point, so set orderedItems back to null
 01836                    orderedItems = null;
 1837                }
 01838                else if (orderedItems is null)
 1839                {
 01840                    orderedItems = sortOrder == SortOrder.Descending
 01841                        ? items.OrderByDescending(i => i, comparer)
 01842                        : items.OrderBy(i => i, comparer);
 1843                }
 1844                else
 1845                {
 01846                    orderedItems = sortOrder == SortOrder.Descending
 01847                        ? orderedItems!.ThenByDescending(i => i, comparer)
 01848                        : orderedItems!.ThenBy(i => i, comparer); // orderedItems is set during the first iteration
 1849                }
 1850            }
 1851
 01852            return orderedItems ?? items;
 1853        }
 1854
 1855        /// <summary>
 1856        /// Gets the comparer.
 1857        /// </summary>
 1858        /// <param name="name">The name.</param>
 1859        /// <param name="user">The user.</param>
 1860        /// <returns>IBaseItemComparer.</returns>
 1861        private IBaseItemComparer? GetComparer(ItemSortBy name, User? user)
 1862        {
 11863            var comparer = Comparers.FirstOrDefault(c => name == c.Type);
 1864
 1865            // If it requires a user, create a new one, and assign the user
 11866            if (comparer is IUserBaseItemComparer)
 1867            {
 01868                var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType())!; // only null fo
 1869
 01870                userComparer.User = user;
 01871                userComparer.UserManager = _userManager;
 01872                userComparer.UserDataRepository = _userDataRepository;
 1873
 01874                return userComparer;
 1875            }
 1876
 11877            return comparer;
 1878        }
 1879
 1880        /// <inheritdoc />
 1881        public void CreateItem(BaseItem item, BaseItem? parent)
 1882        {
 01883            CreateItems(new[] { item }, parent, CancellationToken.None);
 01884        }
 1885
 1886        /// <inheritdoc />
 1887        public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
 1888        {
 21889            _itemRepository.SaveItems(items, cancellationToken);
 1890
 81891            foreach (var item in items)
 1892            {
 21893                RegisterItem(item);
 1894            }
 1895
 21896            if (ItemAdded is not null)
 1897            {
 81898                foreach (var item in items)
 1899                {
 1900                    // With the live tv guide this just creates too much noise
 21901                    if (item.SourceType != SourceType.Library)
 1902                    {
 1903                        continue;
 1904                    }
 1905
 1906                    try
 1907                    {
 21908                        ItemAdded(
 21909                            this,
 21910                            new ItemChangeEventArgs
 21911                            {
 21912                                Item = item,
 21913                                Parent = parent ?? item.GetParent()
 21914                            });
 21915                    }
 01916                    catch (Exception ex)
 1917                    {
 01918                        _logger.LogError(ex, "Error in ItemAdded event handler");
 01919                    }
 1920                }
 1921            }
 21922        }
 1923
 1924        private bool ImageNeedsRefresh(ItemImageInfo image)
 1925        {
 01926            if (image.Path is not null && image.IsLocalFile)
 1927            {
 01928                if (image.Width == 0 || image.Height == 0 || string.IsNullOrEmpty(image.BlurHash))
 1929                {
 01930                    return true;
 1931                }
 1932
 1933                try
 1934                {
 01935                    return _fileSystem.GetLastWriteTimeUtc(image.Path) != image.DateModified;
 1936                }
 01937                catch (Exception ex)
 1938                {
 01939                    _logger.LogError(ex, "Cannot get file info for {0}", image.Path);
 01940                    return false;
 1941                }
 1942            }
 1943
 01944            return image.Path is not null && !image.IsLocalFile;
 01945        }
 1946
 1947        /// <inheritdoc />
 1948        public async Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false)
 1949        {
 1950            ArgumentNullException.ThrowIfNull(item);
 1951
 1952            var outdated = forceUpdate
 1953                ? item.ImageInfos.Where(i => i.Path is not null).ToArray()
 1954                : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
 1955            // Skip image processing if current or live tv source
 1956            if (outdated.Length == 0 || item.SourceType != SourceType.Library)
 1957            {
 1958                RegisterItem(item);
 1959                return;
 1960            }
 1961
 1962            foreach (var img in outdated)
 1963            {
 1964                var image = img;
 1965                if (!img.IsLocalFile)
 1966                {
 1967                    try
 1968                    {
 1969                        var index = item.GetImageIndex(img);
 1970                        image = await ConvertImageToLocal(item, img, index, true).ConfigureAwait(false);
 1971                    }
 1972                    catch (ArgumentException)
 1973                    {
 1974                        _logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
 1975                        continue;
 1976                    }
 1977                    catch (Exception ex) when (ex is InvalidOperationException or IOException)
 1978                    {
 1979                        _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path);
 1980                        continue;
 1981                    }
 1982                    catch (HttpRequestException ex)
 1983                    {
 1984                        _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}. Http status code: {HttpStatus}", im
 1985                        continue;
 1986                    }
 1987                }
 1988
 1989                ImageDimensions size;
 1990                try
 1991                {
 1992                    size = _imageProcessor.GetImageDimensions(item, image);
 1993                    image.Width = size.Width;
 1994                    image.Height = size.Height;
 1995                }
 1996                catch (Exception ex)
 1997                {
 1998                    _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
 1999                    size = default;
 2000                    image.Width = 0;
 2001                    image.Height = 0;
 2002                }
 2003
 2004                try
 2005                {
 2006                    image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path, size);
 2007                }
 2008                catch (Exception ex)
 2009                {
 2010                    _logger.LogError(ex, "Cannot compute blurhash for {ImagePath}", image.Path);
 2011                    image.BlurHash = string.Empty;
 2012                }
 2013
 2014                try
 2015                {
 2016                    image.DateModified = _fileSystem.GetLastWriteTimeUtc(image.Path);
 2017                }
 2018                catch (Exception ex)
 2019                {
 2020                    _logger.LogError(ex, "Cannot update DateModified for {ImagePath}", image.Path);
 2021                }
 2022            }
 2023
 2024            _itemRepository.SaveImages(item);
 2025            RegisterItem(item);
 2026        }
 2027
 2028        /// <inheritdoc />
 2029        public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, 
 2030        {
 2031            _itemRepository.SaveItems(items, cancellationToken);
 2032
 2033            foreach (var item in items)
 2034            {
 2035                await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
 2036            }
 2037
 2038            if (ItemUpdated is not null)
 2039            {
 2040                foreach (var item in items)
 2041                {
 2042                    // With the live tv guide this just creates too much noise
 2043                    if (item.SourceType != SourceType.Library)
 2044                    {
 2045                        continue;
 2046                    }
 2047
 2048                    try
 2049                    {
 2050                        ItemUpdated(
 2051                            this,
 2052                            new ItemChangeEventArgs
 2053                            {
 2054                                Item = item,
 2055                                Parent = parent,
 2056                                UpdateReason = updateReason
 2057                            });
 2058                    }
 2059                    catch (Exception ex)
 2060                    {
 2061                        _logger.LogError(ex, "Error in ItemUpdated event handler");
 2062                    }
 2063                }
 2064            }
 2065        }
 2066
 2067        /// <inheritdoc />
 2068        public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cance
 812069            => UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken);
 2070
 2071        public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
 2072        {
 2073            if (item.IsFileProtocol)
 2074            {
 2075                await ProviderManager.SaveMetadataAsync(item, updateReason).ConfigureAwait(false);
 2076            }
 2077
 2078            item.DateLastSaved = DateTime.UtcNow;
 2079
 2080            await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
 2081        }
 2082
 2083        /// <summary>
 2084        /// Reports the item removed.
 2085        /// </summary>
 2086        /// <param name="item">The item.</param>
 2087        /// <param name="parent">The parent item.</param>
 2088        public void ReportItemRemoved(BaseItem item, BaseItem parent)
 2089        {
 22090            if (ItemRemoved is not null)
 2091            {
 2092                try
 2093                {
 22094                    ItemRemoved(
 22095                        this,
 22096                        new ItemChangeEventArgs
 22097                        {
 22098                            Item = item,
 22099                            Parent = parent
 22100                        });
 22101                }
 02102                catch (Exception ex)
 2103                {
 02104                    _logger.LogError(ex, "Error in ItemRemoved event handler");
 02105                }
 2106            }
 22107        }
 2108
 2109        /// <summary>
 2110        /// Retrieves the item.
 2111        /// </summary>
 2112        /// <param name="id">The id.</param>
 2113        /// <returns>BaseItem.</returns>
 2114        public BaseItem RetrieveItem(Guid id)
 2115        {
 2332116            return _itemRepository.RetrieveItem(id);
 2117        }
 2118
 2119        public List<Folder> GetCollectionFolders(BaseItem item)
 2120        {
 6302121            return GetCollectionFolders(item, GetUserRootFolder().Children.OfType<Folder>());
 2122        }
 2123
 2124        public List<Folder> GetCollectionFolders(BaseItem item, IEnumerable<Folder> allUserRootChildren)
 2125        {
 6682126            while (item is not null)
 2127            {
 6682128                var parent = item.GetParent();
 2129
 6682130                if (parent is AggregateFolder)
 2131                {
 2132                    break;
 2133                }
 2134
 6442135                if (parent is null)
 2136                {
 6062137                    var owner = item.GetOwner();
 2138
 6062139                    if (owner is null)
 2140                    {
 2141                        break;
 2142                    }
 2143
 02144                    item = owner;
 2145                }
 2146                else
 2147                {
 382148                    item = parent;
 2149                }
 2150            }
 2151
 6302152            if (item is null)
 2153            {
 02154                return new List<Folder>();
 2155            }
 2156
 6302157            return GetCollectionFoldersInternal(item, allUserRootChildren);
 2158        }
 2159
 2160        private static List<Folder> GetCollectionFoldersInternal(BaseItem item, IEnumerable<Folder> allUserRootChildren)
 2161        {
 6302162            return allUserRootChildren
 6302163                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.
 6302164                .ToList();
 2165        }
 2166
 2167        public LibraryOptions GetLibraryOptions(BaseItem item)
 2168        {
 4122169            if (item is CollectionFolder collectionFolder)
 2170            {
 572171                return collectionFolder.GetLibraryOptions();
 2172            }
 2173
 2174            // List.Find is more performant than FirstOrDefault due to enumerator allocation
 3552175            return GetCollectionFolders(item)
 3552176                .Find(folder => folder is CollectionFolder) is CollectionFolder collectionFolder2
 3552177                ? collectionFolder2.GetLibraryOptions()
 3552178                : new LibraryOptions();
 2179        }
 2180
 2181        public CollectionType? GetContentType(BaseItem item)
 2182        {
 422183            var configuredContentType = GetConfiguredContentType(item, false);
 422184            if (configuredContentType is not null)
 2185            {
 02186                return configuredContentType;
 2187            }
 2188
 422189            configuredContentType = GetConfiguredContentType(item, true);
 422190            if (configuredContentType is not null)
 2191            {
 02192                return configuredContentType;
 2193            }
 2194
 422195            return GetInheritedContentType(item);
 2196        }
 2197
 2198        public CollectionType? GetInheritedContentType(BaseItem item)
 2199        {
 422200            var type = GetTopFolderContentType(item);
 2201
 422202            if (type is not null)
 2203            {
 02204                return type;
 2205            }
 2206
 422207            return item.GetParents()
 422208                .Select(GetConfiguredContentType)
 422209                .LastOrDefault(i => i is not null);
 2210        }
 2211
 2212        public CollectionType? GetConfiguredContentType(BaseItem item)
 2213        {
 02214            return GetConfiguredContentType(item, false);
 2215        }
 2216
 2217        public CollectionType? GetConfiguredContentType(string path)
 2218        {
 02219            return GetContentTypeOverride(path, false);
 2220        }
 2221
 2222        public CollectionType? GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath)
 2223        {
 842224            if (item is ICollectionFolder collectionFolder)
 2225            {
 02226                return collectionFolder.CollectionType;
 2227            }
 2228
 842229            return GetContentTypeOverride(item.ContainingFolderPath, inheritConfiguredPath);
 2230        }
 2231
 2232        private CollectionType? GetContentTypeOverride(string path, bool inherit)
 2233        {
 992234            var nameValuePair = _configurationManager.Configuration.ContentTypes
 992235                                    .FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path)
 992236                                                         || (inherit && !string.IsNullOrEmpty(i.Name)
 992237                                                                     && _fileSystem.ContainsSubPath(i.Name, path)));
 992238            if (Enum.TryParse<CollectionType>(nameValuePair?.Value, out var collectionType))
 2239            {
 02240                return collectionType;
 2241            }
 2242
 992243            return null;
 2244        }
 2245
 2246        private CollectionType? GetTopFolderContentType(BaseItem item)
 2247        {
 422248            if (item is null)
 2249            {
 02250                return null;
 2251            }
 2252
 422253            while (!item.ParentId.IsEmpty())
 2254            {
 02255                var parent = item.GetParent();
 02256                if (parent is null || parent is AggregateFolder)
 2257                {
 2258                    break;
 2259                }
 2260
 02261                item = parent;
 2262            }
 2263
 422264            return GetUserRootFolder().Children
 422265                .OfType<ICollectionFolder>()
 422266                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.
 422267                .Select(i => i.CollectionType)
 422268                .FirstOrDefault(i => i is not null);
 2269        }
 2270
 2271        public UserView GetNamedView(
 2272            User user,
 2273            string name,
 2274            CollectionType? viewType,
 2275            string sortName)
 2276        {
 02277            return GetNamedView(user, name, Guid.Empty, viewType, sortName);
 2278        }
 2279
 2280        public UserView GetNamedView(
 2281            string name,
 2282            CollectionType viewType,
 2283            string sortName)
 2284        {
 02285            var path = Path.Combine(
 02286                _configurationManager.ApplicationPaths.InternalMetadataPath,
 02287                "views",
 02288                _fileSystem.GetValidFilename(viewType.ToString()));
 2289
 02290            var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView));
 2291
 02292            var item = GetItemById(id) as UserView;
 2293
 02294            var refresh = false;
 2295
 02296            if (item is null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase))
 2297            {
 02298                Directory.CreateDirectory(path);
 2299
 02300                item = new UserView
 02301                {
 02302                    Path = path,
 02303                    Id = id,
 02304                    DateCreated = DateTime.UtcNow,
 02305                    Name = name,
 02306                    ViewType = viewType,
 02307                    ForcedSortName = sortName
 02308                };
 2309
 02310                CreateItem(item, null);
 2311
 02312                refresh = true;
 2313            }
 2314
 02315            if (refresh)
 2316            {
 02317                item.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResu
 02318                ProviderManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), Ref
 2319            }
 2320
 02321            return item;
 2322        }
 2323
 2324        public UserView GetNamedView(
 2325            User user,
 2326            string name,
 2327            Guid parentId,
 2328            CollectionType? viewType,
 2329            string sortName)
 2330        {
 02331            var parentIdString = parentId.IsEmpty()
 02332                ? null
 02333                : parentId.ToString("N", CultureInfo.InvariantCulture);
 02334            var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdStrin
 2335
 02336            var id = GetNewItemId(idValues, typeof(UserView));
 2337
 02338            var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N
 2339
 02340            var item = GetItemById(id) as UserView;
 2341
 02342            var isNew = false;
 2343
 02344            if (item is null)
 2345            {
 02346                Directory.CreateDirectory(path);
 2347
 02348                item = new UserView
 02349                {
 02350                    Path = path,
 02351                    Id = id,
 02352                    DateCreated = DateTime.UtcNow,
 02353                    Name = name,
 02354                    ViewType = viewType,
 02355                    ForcedSortName = sortName,
 02356                    UserId = user.Id,
 02357                    DisplayParentId = parentId
 02358                };
 2359
 02360                CreateItem(item, null);
 2361
 02362                isNew = true;
 2363            }
 2364
 02365            var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
 2366
 02367            if (!refresh && !item.DisplayParentId.IsEmpty())
 2368            {
 02369                var displayParent = GetItemById(item.DisplayParentId);
 02370                refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed;
 2371            }
 2372
 02373            if (refresh)
 2374            {
 02375                ProviderManager.QueueRefresh(
 02376                    item.Id,
 02377                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 02378                    {
 02379                        // Need to force save to increment DateLastSaved
 02380                        ForceSave = true
 02381                    },
 02382                    RefreshPriority.Normal);
 2383            }
 2384
 02385            return item;
 2386        }
 2387
 2388        public UserView GetShadowView(
 2389            BaseItem parent,
 2390            CollectionType? viewType,
 2391            string sortName)
 2392        {
 02393            ArgumentNullException.ThrowIfNull(parent);
 2394
 02395            var name = parent.Name;
 02396            var parentId = parent.Id;
 2397
 02398            var idValues = "38_namedview_" + name + parentId + (viewType?.ToString() ?? string.Empty);
 2399
 02400            var id = GetNewItemId(idValues, typeof(UserView));
 2401
 02402            var path = parent.Path;
 2403
 02404            var item = GetItemById(id) as UserView;
 2405
 02406            var isNew = false;
 2407
 02408            if (item is null)
 2409            {
 02410                Directory.CreateDirectory(path);
 2411
 02412                item = new UserView
 02413                {
 02414                    Path = path,
 02415                    Id = id,
 02416                    DateCreated = DateTime.UtcNow,
 02417                    Name = name,
 02418                    ViewType = viewType,
 02419                    ForcedSortName = sortName
 02420                };
 2421
 02422                item.DisplayParentId = parentId;
 2423
 02424                CreateItem(item, null);
 2425
 02426                isNew = true;
 2427            }
 2428
 02429            var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
 2430
 02431            if (!refresh && !item.DisplayParentId.IsEmpty())
 2432            {
 02433                var displayParent = GetItemById(item.DisplayParentId);
 02434                refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed;
 2435            }
 2436
 02437            if (refresh)
 2438            {
 02439                ProviderManager.QueueRefresh(
 02440                    item.Id,
 02441                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 02442                    {
 02443                        // Need to force save to increment DateLastSaved
 02444                        ForceSave = true
 02445                    },
 02446                    RefreshPriority.Normal);
 2447            }
 2448
 02449            return item;
 2450        }
 2451
 2452        public UserView GetNamedView(
 2453            string name,
 2454            Guid parentId,
 2455            CollectionType? viewType,
 2456            string sortName,
 2457            string uniqueId)
 2458        {
 02459            ArgumentException.ThrowIfNullOrEmpty(name);
 2460
 02461            var parentIdString = parentId.IsEmpty()
 02462                ? null
 02463                : parentId.ToString("N", CultureInfo.InvariantCulture);
 02464            var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.E
 02465            if (!string.IsNullOrEmpty(uniqueId))
 2466            {
 02467                idValues += uniqueId;
 2468            }
 2469
 02470            var id = GetNewItemId(idValues, typeof(UserView));
 2471
 02472            var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N
 2473
 02474            var item = GetItemById(id) as UserView;
 2475
 02476            var isNew = false;
 2477
 02478            if (item is null)
 2479            {
 02480                Directory.CreateDirectory(path);
 2481
 02482                item = new UserView
 02483                {
 02484                    Path = path,
 02485                    Id = id,
 02486                    DateCreated = DateTime.UtcNow,
 02487                    Name = name,
 02488                    ViewType = viewType,
 02489                    ForcedSortName = sortName
 02490                };
 2491
 02492                item.DisplayParentId = parentId;
 2493
 02494                CreateItem(item, null);
 2495
 02496                isNew = true;
 2497            }
 2498
 02499            if (viewType != item.ViewType)
 2500            {
 02501                item.ViewType = viewType;
 02502                item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult
 2503            }
 2504
 02505            var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
 2506
 02507            if (!refresh && !item.DisplayParentId.IsEmpty())
 2508            {
 02509                var displayParent = GetItemById(item.DisplayParentId);
 02510                refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed;
 2511            }
 2512
 02513            if (refresh)
 2514            {
 02515                ProviderManager.QueueRefresh(
 02516                    item.Id,
 02517                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 02518                    {
 02519                        // Need to force save to increment DateLastSaved
 02520                        ForceSave = true
 02521                    },
 02522                    RefreshPriority.Normal);
 2523            }
 2524
 02525            return item;
 2526        }
 2527
 2528        public BaseItem GetParentItem(Guid? parentId, Guid? userId)
 2529        {
 32530            if (parentId.HasValue)
 2531            {
 02532                return GetItemById(parentId.Value) ?? throw new ArgumentException($"Invalid parent id: {parentId.Value}"
 2533            }
 2534
 32535            if (!userId.IsNullOrEmpty())
 2536            {
 32537                return GetUserRootFolder();
 2538            }
 2539
 02540            return RootFolder;
 2541        }
 2542
 2543        /// <inheritdoc />
 2544        public void QueueLibraryScan()
 2545        {
 02546            _taskManager.QueueScheduledTask<RefreshMediaLibraryTask>();
 02547        }
 2548
 2549        /// <inheritdoc />
 2550        public int? GetSeasonNumberFromPath(string path, Guid? parentId)
 2551        {
 02552            var parentPath = parentId.HasValue ? GetItemById(parentId.Value)?.ContainingFolderPath : null;
 02553            return SeasonPathParser.Parse(path, parentPath, true, true).SeasonNumber;
 2554        }
 2555
 2556        /// <inheritdoc />
 2557        public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
 2558        {
 02559            var series = episode.Series;
 02560            bool? isAbsoluteNaming = series is not null && string.Equals(series.DisplayOrder, "absolute", StringComparis
 02561            if (!isAbsoluteNaming.Value)
 2562            {
 2563                // In other words, no filter applied
 02564                isAbsoluteNaming = null;
 2565            }
 2566
 02567            var resolver = new EpisodeResolver(_namingOptions);
 2568
 02569            var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
 2570
 2571            // TODO nullable - what are we trying to do there with empty episodeInfo?
 02572            EpisodeInfo? episodeInfo = null;
 02573            if (episode.IsFileProtocol)
 2574            {
 02575                episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming);
 2576                // Resolve from parent folder if it's not the Season folder
 02577                var parent = episode.GetParent();
 02578                if (episodeInfo is null && parent.GetType() == typeof(Folder))
 2579                {
 02580                    episodeInfo = resolver.Resolve(parent.Path, true, null, null, isAbsoluteNaming);
 02581                    if (episodeInfo is not null)
 2582                    {
 2583                        // add the container
 02584                        episodeInfo.Container = Path.GetExtension(episode.Path)?.TrimStart('.');
 2585                    }
 2586                }
 2587            }
 2588
 02589            episodeInfo ??= new EpisodeInfo(episode.Path);
 2590
 2591            try
 2592            {
 02593                var libraryOptions = GetLibraryOptions(episode);
 02594                if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringCompa
 2595                {
 2596                    // Read from metadata
 02597                    var mediaInfo = _mediaEncoder.GetMediaInfo(
 02598                        new MediaInfoRequest
 02599                        {
 02600                            MediaSource = episode.GetMediaSources(false)[0],
 02601                            MediaType = DlnaProfileType.Video
 02602                        },
 02603                        CancellationToken.None).GetAwaiter().GetResult();
 02604                    if (mediaInfo.ParentIndexNumber > 0)
 2605                    {
 02606                        episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber;
 2607                    }
 2608
 02609                    if (mediaInfo.IndexNumber > 0)
 2610                    {
 02611                        episodeInfo.EpisodeNumber = mediaInfo.IndexNumber;
 2612                    }
 2613
 02614                    if (!string.IsNullOrEmpty(mediaInfo.ShowName))
 2615                    {
 02616                        episodeInfo.SeriesName = mediaInfo.ShowName;
 2617                    }
 2618                }
 02619            }
 02620            catch (Exception ex)
 2621            {
 02622                _logger.LogError(ex, "Error reading the episode information with ffprobe. Episode: {EpisodeInfo}", episo
 02623            }
 2624
 02625            var changed = false;
 2626
 02627            if (episodeInfo.IsByDate)
 2628            {
 02629                if (episode.IndexNumber.HasValue)
 2630                {
 02631                    episode.IndexNumber = null;
 02632                    changed = true;
 2633                }
 2634
 02635                if (episode.IndexNumberEnd.HasValue)
 2636                {
 02637                    episode.IndexNumberEnd = null;
 02638                    changed = true;
 2639                }
 2640
 02641                if (!episode.PremiereDate.HasValue)
 2642                {
 02643                    if (episodeInfo.Year.HasValue && episodeInfo.Month.HasValue && episodeInfo.Day.HasValue)
 2644                    {
 02645                        episode.PremiereDate = new DateTime(episodeInfo.Year.Value, episodeInfo.Month.Value, episodeInfo
 2646                    }
 2647
 02648                    if (episode.PremiereDate.HasValue)
 2649                    {
 02650                        changed = true;
 2651                    }
 2652                }
 2653
 02654                if (!episode.ProductionYear.HasValue)
 2655                {
 02656                    episode.ProductionYear = episodeInfo.Year;
 2657
 02658                    if (episode.ProductionYear.HasValue)
 2659                    {
 02660                        changed = true;
 2661                    }
 2662                }
 2663            }
 2664            else
 2665            {
 02666                if (!episode.IndexNumber.HasValue || forceRefresh)
 2667                {
 02668                    if (episode.IndexNumber != episodeInfo.EpisodeNumber)
 2669                    {
 02670                        changed = true;
 2671                    }
 2672
 02673                    episode.IndexNumber = episodeInfo.EpisodeNumber;
 2674                }
 2675
 02676                if (!episode.IndexNumberEnd.HasValue || forceRefresh)
 2677                {
 02678                    if (episode.IndexNumberEnd != episodeInfo.EndingEpisodeNumber)
 2679                    {
 02680                        changed = true;
 2681                    }
 2682
 02683                    episode.IndexNumberEnd = episodeInfo.EndingEpisodeNumber;
 2684                }
 2685
 02686                if (!episode.ParentIndexNumber.HasValue || forceRefresh)
 2687                {
 02688                    if (episode.ParentIndexNumber != episodeInfo.SeasonNumber)
 2689                    {
 02690                        changed = true;
 2691                    }
 2692
 02693                    episode.ParentIndexNumber = episodeInfo.SeasonNumber;
 2694                }
 2695            }
 2696
 02697            if (!episode.ParentIndexNumber.HasValue)
 2698            {
 02699                var season = episode.Season;
 2700
 02701                if (season is not null)
 2702                {
 02703                    episode.ParentIndexNumber = season.IndexNumber;
 2704                }
 2705
 02706                if (episode.ParentIndexNumber.HasValue)
 2707                {
 02708                    changed = true;
 2709                }
 2710            }
 2711
 02712            return changed;
 2713        }
 2714
 2715        public ItemLookupInfo ParseName(string name)
 2716        {
 02717            var namingOptions = _namingOptions;
 02718            var result = VideoResolver.CleanDateTime(name, namingOptions);
 2719
 02720            return new ItemLookupInfo
 02721            {
 02722                Name = VideoResolver.TryCleanString(result.Name, namingOptions, out var newName) ? newName : result.Name
 02723                Year = result.Year
 02724            };
 2725        }
 2726
 2727        public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, ID
 2728        {
 2729            var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions, libraryRoot: owner.Co
 2730            if (ownerVideoInfo is null)
 2731            {
 2732                yield break;
 2733            }
 2734
 2735            var count = fileSystemChildren.Count;
 2736            for (var i = 0; i < count; i++)
 2737            {
 2738                var current = fileSystemChildren[i];
 2739                if (current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name))
 2740                {
 2741                    var filesInSubFolder = _fileSystem.GetFiles(current.FullName, null, false, false);
 2742                    foreach (var file in filesInSubFolder)
 2743                    {
 2744                        if (!_extraResolver.TryGetExtraTypeForOwner(file.FullName, ownerVideoInfo, out var extraType))
 2745                        {
 2746                            continue;
 2747                        }
 2748
 2749                        var extra = GetExtra(file, extraType.Value);
 2750                        if (extra is not null)
 2751                        {
 2752                            yield return extra;
 2753                        }
 2754                    }
 2755                }
 2756                else if (!current.IsDirectory && _extraResolver.TryGetExtraTypeForOwner(current.FullName, ownerVideoInfo
 2757                {
 2758                    var extra = GetExtra(current, extraType.Value);
 2759                    if (extra is not null)
 2760                    {
 2761                        yield return extra;
 2762                    }
 2763                }
 2764            }
 2765
 2766            BaseItem? GetExtra(FileSystemMetadata file, ExtraType extraType)
 2767            {
 2768                var extra = ResolvePath(_fileSystem.GetFileInfo(file.FullName), directoryService, _extraResolver.GetReso
 2769                if (extra is not Video && extra is not Audio)
 2770                {
 2771                    return null;
 2772                }
 2773
 2774                // Try to retrieve it from the db. If we don't find it, use the resolved version
 2775                var itemById = GetItemById(extra.Id);
 2776                if (itemById is not null)
 2777                {
 2778                    extra = itemById;
 2779                }
 2780
 2781                // Only update extra type if it is more specific then the currently known extra type
 2782                if (extra.ExtraType is null or ExtraType.Unknown || extraType != ExtraType.Unknown)
 2783                {
 2784                    extra.ExtraType = extraType;
 2785                }
 2786
 2787                extra.ParentId = Guid.Empty;
 2788                extra.OwnerId = owner.Id;
 2789                return extra;
 2790            }
 2791        }
 2792
 2793        public string GetPathAfterNetworkSubstitution(string path, BaseItem? ownerItem)
 2794        {
 122795            foreach (var map in _configurationManager.Configuration.PathSubstitutions)
 2796            {
 02797                if (path.TryReplaceSubPath(map.From, map.To, out var newPath))
 2798                {
 02799                    return newPath;
 2800                }
 2801            }
 2802
 62803            return path;
 2804        }
 2805
 2806        public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query)
 2807        {
 02808            return _peopleRepository.GetPeople(query);
 2809        }
 2810
 2811        public IReadOnlyList<PersonInfo> GetPeople(BaseItem item)
 2812        {
 572813            if (item.SupportsPeople)
 2814            {
 02815                var people = GetPeople(new InternalPeopleQuery
 02816                {
 02817                    ItemId = item.Id
 02818                });
 2819
 02820                if (people.Count > 0)
 2821                {
 02822                    return people;
 2823                }
 2824            }
 2825
 572826            return [];
 2827        }
 2828
 2829        public IReadOnlyList<Person> GetPeopleItems(InternalPeopleQuery query)
 2830        {
 02831            return _peopleRepository.GetPeopleNames(query)
 02832            .Select(i =>
 02833            {
 02834                try
 02835                {
 02836                    return GetPerson(i);
 02837                }
 02838                catch (Exception ex)
 02839                {
 02840                    _logger.LogError(ex, "Error getting person");
 02841                    return null;
 02842                }
 02843            })
 02844            .Where(i => i is not null)
 02845            .Where(i => query.User is null || i!.IsVisible(query.User))
 02846            .ToList()!; // null values are filtered out
 2847        }
 2848
 2849        public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query)
 2850        {
 02851            return _peopleRepository.GetPeopleNames(query);
 2852        }
 2853
 2854        public void UpdatePeople(BaseItem item, List<PersonInfo> people)
 2855        {
 02856            UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult();
 02857        }
 2858
 2859        /// <inheritdoc />
 2860        public async Task UpdatePeopleAsync(BaseItem item, IReadOnlyList<PersonInfo> people, CancellationToken cancellat
 2861        {
 2862            if (!item.SupportsPeople)
 2863            {
 2864                return;
 2865            }
 2866
 2867            if (people is not null)
 2868            {
 2869                people = people.Where(e => e is not null).ToArray();
 2870                _peopleRepository.UpdatePeople(item.Id, people);
 2871                await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
 2872            }
 2873        }
 2874
 2875        public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex, bool re
 2876        {
 2877            foreach (var url in image.Path.Split('|'))
 2878            {
 2879                try
 2880                {
 2881                    _logger.LogDebug("ConvertImageToLocal item {0} - image url: {1}", item.Id, url);
 2882
 2883                    await ProviderManager.SaveImage(item, url, image.Type, imageIndex, CancellationToken.None).Configure
 2884
 2885                    await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwai
 2886
 2887                    return item.GetImageInfo(image.Type, imageIndex);
 2888                }
 2889                catch (HttpRequestException ex)
 2890                {
 2891                    if (ex.StatusCode.HasValue
 2892                        && (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forb
 2893                    {
 2894                        _logger.LogDebug(ex, "Error downloading image {Url}", url);
 2895                        continue;
 2896                    }
 2897
 2898                    throw;
 2899                }
 2900            }
 2901
 2902            if (removeOnFailure)
 2903            {
 2904                // Remove this image to prevent it from retrying over and over
 2905                item.RemoveImage(image);
 2906                await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(fa
 2907            }
 2908
 2909            throw new InvalidOperationException("Unable to convert any images to local");
 2910        }
 2911
 2912        public async Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, b
 2913        {
 2914            if (string.IsNullOrWhiteSpace(name))
 2915            {
 2916                throw new ArgumentNullException(nameof(name));
 2917            }
 2918
 2919            name = _fileSystem.GetValidFilename(name.Trim());
 2920
 2921            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 2922
 2923            var existingNameCount = 1; // first numbered name will be 2
 2924            var virtualFolderPath = Path.Combine(rootFolderPath, name);
 2925            var originalName = name;
 2926            while (Directory.Exists(virtualFolderPath))
 2927            {
 2928                existingNameCount++;
 2929                name = originalName + existingNameCount;
 2930                virtualFolderPath = Path.Combine(rootFolderPath, name);
 2931            }
 2932
 2933            var mediaPathInfos = options.PathInfos;
 2934            if (mediaPathInfos is not null)
 2935            {
 2936                var invalidpath = mediaPathInfos.FirstOrDefault(i => !Directory.Exists(i.Path));
 2937                if (invalidpath is not null)
 2938                {
 2939                    throw new ArgumentException("The specified path does not exist: " + invalidpath.Path + ".");
 2940                }
 2941            }
 2942
 2943            LibraryMonitor.Stop();
 2944
 2945            try
 2946            {
 2947                Directory.CreateDirectory(virtualFolderPath);
 2948
 2949                if (collectionType is not null)
 2950                {
 2951                    var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collect
 2952
 2953                    await File.WriteAllBytesAsync(path, []).ConfigureAwait(false);
 2954                }
 2955
 2956                CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
 2957
 2958                if (mediaPathInfos is not null)
 2959                {
 2960                    foreach (var path in mediaPathInfos)
 2961                    {
 2962                        AddMediaPathInternal(name, path, false);
 2963                    }
 2964                }
 2965            }
 2966            finally
 2967            {
 2968                if (refreshLibrary)
 2969                {
 2970                    await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false);
 2971
 2972                    StartScanInBackground();
 2973                }
 2974                else
 2975                {
 2976                    // Need to add a delay here or directory watchers may still pick up the changes
 2977                    await Task.Delay(1000).ConfigureAwait(false);
 2978                    LibraryMonitor.Start();
 2979                }
 2980            }
 2981        }
 2982
 2983        private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
 2984        {
 2985            foreach (var person in people)
 2986            {
 2987                cancellationToken.ThrowIfCancellationRequested();
 2988
 2989                var itemUpdateType = ItemUpdateType.MetadataDownload;
 2990                var saveEntity = false;
 2991                var createEntity = false;
 2992                var personEntity = GetPerson(person.Name);
 2993
 2994                if (personEntity is null)
 2995                {
 2996                    var path = Person.GetPath(person.Name);
 2997                    personEntity = new Person()
 2998                    {
 2999                        Name = person.Name,
 3000                        Id = GetItemByNameId<Person>(path),
 3001                        DateCreated = DateTime.UtcNow,
 3002                        DateModified = DateTime.UtcNow,
 3003                        Path = path
 3004                    };
 3005
 3006                    personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
 3007                    saveEntity = true;
 3008                    createEntity = true;
 3009                }
 3010
 3011                foreach (var id in person.ProviderIds)
 3012                {
 3013                    if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase)
 3014                    {
 3015                        personEntity.SetProviderId(id.Key, id.Value);
 3016                        saveEntity = true;
 3017                    }
 3018                }
 3019
 3020                if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary))
 3021                {
 3022                    personEntity.SetImage(
 3023                        new ItemImageInfo
 3024                        {
 3025                            Path = person.ImageUrl,
 3026                            Type = ImageType.Primary
 3027                        },
 3028                        0);
 3029
 3030                    saveEntity = true;
 3031                    itemUpdateType = ItemUpdateType.ImageUpdate;
 3032                }
 3033
 3034                if (saveEntity)
 3035                {
 3036                    if (createEntity)
 3037                    {
 3038                        CreateItems([personEntity], null, CancellationToken.None);
 3039                    }
 3040
 3041                    await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
 3042                    CreateItems([personEntity], null, CancellationToken.None);
 3043                }
 3044            }
 3045        }
 3046
 3047        private void StartScanInBackground()
 3048        {
 33049            Task.Run(() =>
 33050            {
 33051                // No need to start if scanning the library because it will handle it
 33052                ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
 33053            });
 33054        }
 3055
 3056        public void AddMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
 3057        {
 13058            AddMediaPathInternal(virtualFolderName, mediaPath, true);
 03059        }
 3060
 3061        private void AddMediaPathInternal(string virtualFolderName, MediaPathInfo pathInfo, bool saveLibraryOptions)
 3062        {
 13063            ArgumentNullException.ThrowIfNull(pathInfo);
 3064
 13065            var path = pathInfo.Path;
 3066
 13067            if (string.IsNullOrWhiteSpace(path))
 3068            {
 03069                throw new ArgumentException(nameof(path));
 3070            }
 3071
 13072            if (!Directory.Exists(path))
 3073            {
 13074                throw new FileNotFoundException("The path does not exist.");
 3075            }
 3076
 03077            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 03078            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3079
 03080            var shortcutFilename = Path.GetFileNameWithoutExtension(path);
 3081
 03082            var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
 3083
 03084            while (File.Exists(lnk))
 3085            {
 03086                shortcutFilename += "1";
 03087                lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
 3088            }
 3089
 03090            _fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
 3091
 03092            RemoveContentTypeOverrides(path);
 3093
 03094            if (saveLibraryOptions)
 3095            {
 03096                var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3097
 03098                libraryOptions.PathInfos = [.. libraryOptions.PathInfos, pathInfo];
 3099
 03100                SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
 3101
 03102                CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 3103            }
 03104        }
 3105
 3106        public void UpdateMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
 3107        {
 03108            ArgumentNullException.ThrowIfNull(mediaPath);
 3109
 03110            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 03111            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3112
 03113            var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3114
 03115            SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
 3116
 03117            CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 03118        }
 3119
 3120        private void SyncLibraryOptionsToLocations(string virtualFolderPath, LibraryOptions options)
 3121        {
 03122            var topLibraryFolders = GetUserRootFolder().Children.ToList();
 03123            var info = GetVirtualFolderInfo(virtualFolderPath, topLibraryFolders, null);
 3124
 03125            if (info.Locations.Length > 0 && info.Locations.Length != options.PathInfos.Length)
 3126            {
 03127                var list = options.PathInfos.ToList();
 3128
 03129                foreach (var location in info.Locations)
 3130                {
 03131                    if (!list.Any(i => string.Equals(i.Path, location, StringComparison.Ordinal)))
 3132                    {
 03133                        list.Add(new MediaPathInfo(location));
 3134                    }
 3135                }
 3136
 03137                options.PathInfos = list.ToArray();
 3138            }
 03139        }
 3140
 3141        public async Task RemoveVirtualFolder(string name, bool refreshLibrary)
 3142        {
 3143            if (string.IsNullOrWhiteSpace(name))
 3144            {
 3145                throw new ArgumentNullException(nameof(name));
 3146            }
 3147
 3148            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 3149
 3150            var path = Path.Combine(rootFolderPath, name);
 3151
 3152            if (!Directory.Exists(path))
 3153            {
 3154                throw new FileNotFoundException("The media folder does not exist");
 3155            }
 3156
 3157            LibraryMonitor.Stop();
 3158
 3159            try
 3160            {
 3161                Directory.Delete(path, true);
 3162            }
 3163            finally
 3164            {
 3165                CollectionFolder.OnCollectionFolderChange();
 3166
 3167                if (refreshLibrary)
 3168                {
 3169                    await ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
 3170
 3171                    StartScanInBackground();
 3172                }
 3173                else
 3174                {
 3175                    // Need to add a delay here or directory watchers may still pick up the changes
 3176                    await Task.Delay(1000).ConfigureAwait(false);
 3177                    LibraryMonitor.Start();
 3178                }
 3179            }
 3180        }
 3181
 3182        private void RemoveContentTypeOverrides(string path)
 3183        {
 03184            if (string.IsNullOrWhiteSpace(path))
 3185            {
 03186                throw new ArgumentNullException(nameof(path));
 3187            }
 3188
 03189            List<NameValuePair>? removeList = null;
 3190
 03191            foreach (var contentType in _configurationManager.Configuration.ContentTypes)
 3192            {
 03193                if (string.IsNullOrWhiteSpace(contentType.Name)
 03194                    || _fileSystem.AreEqual(path, contentType.Name)
 03195                    || _fileSystem.ContainsSubPath(path, contentType.Name))
 3196                {
 03197                    (removeList ??= new()).Add(contentType);
 3198                }
 3199            }
 3200
 03201            if (removeList is not null)
 3202            {
 03203                _configurationManager.Configuration.ContentTypes = _configurationManager.Configuration.ContentTypes
 03204                    .Except(removeList)
 03205                    .ToArray();
 3206
 03207                _configurationManager.SaveConfiguration();
 3208            }
 03209        }
 3210
 3211        public void RemoveMediaPath(string virtualFolderName, string mediaPath)
 3212        {
 13213            ArgumentException.ThrowIfNullOrEmpty(mediaPath);
 3214
 13215            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 13216            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3217
 13218            if (!Directory.Exists(virtualFolderPath))
 3219            {
 13220                throw new FileNotFoundException(
 13221                    string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolder
 3222            }
 3223
 03224            var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
 03225                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCa
 03226                .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, String
 3227
 03228            if (!string.IsNullOrEmpty(shortcut))
 3229            {
 03230                _fileSystem.DeleteFile(shortcut);
 3231            }
 3232
 03233            var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3234
 03235            libraryOptions.PathInfos = libraryOptions
 03236                .PathInfos
 03237                .Where(i => !string.Equals(i.Path, mediaPath, StringComparison.Ordinal))
 03238                .ToArray();
 3239
 03240            CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 03241        }
 3242
 3243        private static bool ItemIsVisible(BaseItem? item, User? user)
 3244        {
 213245            if (item is null)
 3246            {
 213247                return false;
 3248            }
 3249
 03250            if (user is null)
 3251            {
 03252                return true;
 3253            }
 3254
 03255            return item is UserRootFolder || item.IsVisibleStandalone(user);
 3256        }
 3257    }
 3258}

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,MediaBrowser.Controller.Persistence.IPeopleRepository,MediaBrowser.Controller.IO.IPathManager)
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>)
GetInternalMetadataPaths(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.Database.Implementations.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>)
GetLatestItemList(MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>,Jellyfin.Data.Enums.CollectionType)
GetNextUpSeriesKeys(MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.IReadOnlyCollection`1<MediaBrowser.Controller.Entities.BaseItem>,System.DateTime)
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.Database.Implementations.Entities.User,System.Boolean)
GetTopParentIdsForQuery(MediaBrowser.Controller.Entities.BaseItem,Jellyfin.Database.Implementations.Entities.User)
ResolveIntro(MediaBrowser.Controller.Library.IntroInfo)
Sort(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.BaseItem>,Jellyfin.Database.Implementations.Entities.User,System.Collections.Generic.IEnumerable`1<Jellyfin.Data.Enums.ItemSortBy>,Jellyfin.Database.Implementations.Enums.SortOrder)
Sort(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Entities.BaseItem>,Jellyfin.Database.Implementations.Entities.User,System.Collections.Generic.IEnumerable`1<System.ValueTuple`2<Jellyfin.Data.Enums.ItemSortBy,Jellyfin.Database.Implementations.Enums.SortOrder>>)
GetComparer(Jellyfin.Data.Enums.ItemSortBy,Jellyfin.Database.Implementations.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.Database.Implementations.Entities.User,System.String,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>,System.String)
GetNamedView(System.String,Jellyfin.Data.Enums.CollectionType,System.String)
GetNamedView(Jellyfin.Database.Implementations.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,System.Nullable`1<System.Guid>)
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.Database.Implementations.Entities.User)