< 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: 428
Uncovered lines: 657
Coverable lines: 1085
Total lines: 3294
Line coverage: 39.4%
Branch coverage
32%
Covered branches: 202
Total branches: 616
Branch coverage: 32.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(...)25%10204825%
IsInternalItem(...)50%741844.44%
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%242078.12%
IgnoreFile(...)100%11100%
NormalizeRootPathList(...)50%2291.66%
ResolvePaths(...)100%11100%
ResolvePaths(...)58.33%221258.33%
CreateRootFolder()85.71%141492.3%
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%
GetItemCounts(...)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%3192560%
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.MediaSegments;
 38using MediaBrowser.Controller.Persistence;
 39using MediaBrowser.Controller.Providers;
 40using MediaBrowser.Controller.Resolvers;
 41using MediaBrowser.Controller.Sorting;
 42using MediaBrowser.Controller.Trickplay;
 43using MediaBrowser.Model.Configuration;
 44using MediaBrowser.Model.Dlna;
 45using MediaBrowser.Model.Drawing;
 46using MediaBrowser.Model.Dto;
 47using MediaBrowser.Model.Entities;
 48using MediaBrowser.Model.IO;
 49using MediaBrowser.Model.Library;
 50using MediaBrowser.Model.Querying;
 51using MediaBrowser.Model.Tasks;
 52using Microsoft.Extensions.Logging;
 53using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 54using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
 55using Genre = MediaBrowser.Controller.Entities.Genre;
 56using Person = MediaBrowser.Controller.Entities.Person;
 57using VideoResolver = Emby.Naming.Video.VideoResolver;
 58
 59namespace Emby.Server.Implementations.Library
 60{
 61    /// <summary>
 62    /// Class LibraryManager.
 63    /// </summary>
 64    public class LibraryManager : ILibraryManager
 65    {
 66        private const string ShortcutFileExtension = ".mblink";
 67
 68        private readonly ILogger<LibraryManager> _logger;
 69        private readonly ITaskManager _taskManager;
 70        private readonly IUserManager _userManager;
 71        private readonly IUserDataManager _userDataManager;
 72        private readonly IServerConfigurationManager _configurationManager;
 73        private readonly Lazy<ILibraryMonitor> _libraryMonitorFactory;
 74        private readonly Lazy<IProviderManager> _providerManagerFactory;
 75        private readonly Lazy<IUserViewManager> _userViewManagerFactory;
 76        private readonly IServerApplicationHost _appHost;
 77        private readonly IMediaEncoder _mediaEncoder;
 78        private readonly IFileSystem _fileSystem;
 79        private readonly IItemRepository _itemRepository;
 80        private readonly IImageProcessor _imageProcessor;
 81        private readonly NamingOptions _namingOptions;
 82        private readonly IPeopleRepository _peopleRepository;
 83        private readonly ExtraResolver _extraResolver;
 84        private readonly IPathManager _pathManager;
 85        private readonly FastConcurrentLru<Guid, BaseItem> _cache;
 86
 87        /// <summary>
 88        /// The _root folder sync lock.
 89        /// </summary>
 2890        private readonly Lock _rootFolderSyncLock = new();
 2891        private readonly Lock _userRootFolderSyncLock = new();
 92
 2893        private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
 94
 95        /// <summary>
 96        /// The _root folder.
 97        /// </summary>
 98        private volatile AggregateFolder? _rootFolder;
 99        private volatile UserRootFolder? _userRootFolder;
 100
 101        private bool _wizardCompleted;
 102
 103        /// <summary>
 104        /// Initializes a new instance of the <see cref="LibraryManager" /> class.
 105        /// </summary>
 106        /// <param name="appHost">The application host.</param>
 107        /// <param name="loggerFactory">The logger factory.</param>
 108        /// <param name="taskManager">The task manager.</param>
 109        /// <param name="userManager">The user manager.</param>
 110        /// <param name="configurationManager">The configuration manager.</param>
 111        /// <param name="userDataManager">The user data manager.</param>
 112        /// <param name="libraryMonitorFactory">The library monitor.</param>
 113        /// <param name="fileSystem">The file system.</param>
 114        /// <param name="providerManagerFactory">The provider manager.</param>
 115        /// <param name="userViewManagerFactory">The user view manager.</param>
 116        /// <param name="mediaEncoder">The media encoder.</param>
 117        /// <param name="itemRepository">The item repository.</param>
 118        /// <param name="imageProcessor">The image processor.</param>
 119        /// <param name="namingOptions">The naming options.</param>
 120        /// <param name="directoryService">The directory service.</param>
 121        /// <param name="peopleRepository">The people repository.</param>
 122        /// <param name="pathManager">The path manager.</param>
 123        public LibraryManager(
 124            IServerApplicationHost appHost,
 125            ILoggerFactory loggerFactory,
 126            ITaskManager taskManager,
 127            IUserManager userManager,
 128            IServerConfigurationManager configurationManager,
 129            IUserDataManager userDataManager,
 130            Lazy<ILibraryMonitor> libraryMonitorFactory,
 131            IFileSystem fileSystem,
 132            Lazy<IProviderManager> providerManagerFactory,
 133            Lazy<IUserViewManager> userViewManagerFactory,
 134            IMediaEncoder mediaEncoder,
 135            IItemRepository itemRepository,
 136            IImageProcessor imageProcessor,
 137            NamingOptions namingOptions,
 138            IDirectoryService directoryService,
 139            IPeopleRepository peopleRepository,
 140            IPathManager pathManager)
 141        {
 28142            _appHost = appHost;
 28143            _logger = loggerFactory.CreateLogger<LibraryManager>();
 28144            _taskManager = taskManager;
 28145            _userManager = userManager;
 28146            _configurationManager = configurationManager;
 28147            _userDataManager = userDataManager;
 28148            _libraryMonitorFactory = libraryMonitorFactory;
 28149            _fileSystem = fileSystem;
 28150            _providerManagerFactory = providerManagerFactory;
 28151            _userViewManagerFactory = userViewManagerFactory;
 28152            _mediaEncoder = mediaEncoder;
 28153            _itemRepository = itemRepository;
 28154            _imageProcessor = imageProcessor;
 155
 28156            _cache = new FastConcurrentLru<Guid, BaseItem>(_configurationManager.Configuration.CacheSize);
 157
 28158            _namingOptions = namingOptions;
 28159            _peopleRepository = peopleRepository;
 28160            _pathManager = pathManager;
 28161            _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryServ
 162
 28163            _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
 164
 28165            RecordConfigurationValues(_configurationManager.Configuration);
 28166        }
 167
 168        /// <summary>
 169        /// Occurs when [item added].
 170        /// </summary>
 171        public event EventHandler<ItemChangeEventArgs>? ItemAdded;
 172
 173        /// <summary>
 174        /// Occurs when [item updated].
 175        /// </summary>
 176        public event EventHandler<ItemChangeEventArgs>? ItemUpdated;
 177
 178        /// <summary>
 179        /// Occurs when [item removed].
 180        /// </summary>
 181        public event EventHandler<ItemChangeEventArgs>? ItemRemoved;
 182
 183        /// <summary>
 184        /// Gets the root folder.
 185        /// </summary>
 186        /// <value>The root folder.</value>
 187        public AggregateFolder RootFolder
 188        {
 189            get
 190            {
 163191                if (_rootFolder is null)
 38192                {
 193                    lock (_rootFolderSyncLock)
 194                    {
 38195                        _rootFolder ??= CreateRootFolder();
 17196                    }
 197                }
 198
 142199                return _rootFolder;
 200            }
 201        }
 202
 41203        private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
 204
 86205        private IProviderManager ProviderManager => _providerManagerFactory.Value;
 206
 1207        private IUserViewManager UserViewManager => _userViewManagerFactory.Value;
 208
 209        /// <summary>
 210        /// Gets or sets the postscan tasks.
 211        /// </summary>
 212        /// <value>The postscan tasks.</value>
 213        private ILibraryPostScanTask[] PostScanTasks { get; set; } = [];
 214
 215        /// <summary>
 216        /// Gets or sets the intro providers.
 217        /// </summary>
 218        /// <value>The intro providers.</value>
 219        private IIntroProvider[] IntroProviders { get; set; } = [];
 220
 221        /// <summary>
 222        /// Gets or sets the list of entity resolution ignore rules.
 223        /// </summary>
 224        /// <value>The entity resolution ignore rules.</value>
 225        private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = [];
 226
 227        /// <summary>
 228        /// Gets or sets the list of currently registered entity resolvers.
 229        /// </summary>
 230        /// <value>The entity resolvers enumerable.</value>
 231        private IItemResolver[] EntityResolvers { get; set; } = [];
 232
 233        private IMultiItemResolver[] MultiItemResolvers { get; set; } = [];
 234
 235        /// <summary>
 236        /// Gets or sets the comparers.
 237        /// </summary>
 238        /// <value>The comparers.</value>
 239        private IBaseItemComparer[] Comparers { get; set; } = [];
 240
 241        public bool IsScanRunning { get; private set; }
 242
 243        /// <summary>
 244        /// Adds the parts.
 245        /// </summary>
 246        /// <param name="rules">The rules.</param>
 247        /// <param name="resolvers">The resolvers.</param>
 248        /// <param name="introProviders">The intro providers.</param>
 249        /// <param name="itemComparers">The item comparers.</param>
 250        /// <param name="postScanTasks">The post scan tasks.</param>
 251        public void AddParts(
 252            IEnumerable<IResolverIgnoreRule> rules,
 253            IEnumerable<IItemResolver> resolvers,
 254            IEnumerable<IIntroProvider> introProviders,
 255            IEnumerable<IBaseItemComparer> itemComparers,
 256            IEnumerable<ILibraryPostScanTask> postScanTasks)
 257        {
 28258            EntityResolutionIgnoreRules = rules.ToArray();
 28259            EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
 28260            MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
 28261            IntroProviders = introProviders.ToArray();
 28262            Comparers = itemComparers.ToArray();
 28263            PostScanTasks = postScanTasks.ToArray();
 28264        }
 265
 266        /// <summary>
 267        /// Records the configuration values.
 268        /// </summary>
 269        /// <param name="configuration">The configuration.</param>
 270        private void RecordConfigurationValues(ServerConfiguration configuration)
 271        {
 129272            _wizardCompleted = configuration.IsStartupWizardCompleted;
 129273        }
 274
 275        /// <summary>
 276        /// Configurations the updated.
 277        /// </summary>
 278        /// <param name="sender">The sender.</param>
 279        /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
 280        private void ConfigurationUpdated(object? sender, EventArgs e)
 281        {
 101282            var config = _configurationManager.Configuration;
 283
 101284            var wizardChanged = config.IsStartupWizardCompleted != _wizardCompleted;
 285
 101286            RecordConfigurationValues(config);
 287
 101288            if (wizardChanged)
 289            {
 16290                _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
 291            }
 101292        }
 293
 294        public void RegisterItem(BaseItem item)
 295        {
 142296            ArgumentNullException.ThrowIfNull(item);
 297
 142298            if (item is IItemByName)
 299            {
 0300                if (item is not MusicArtist)
 301                {
 0302                    return;
 303                }
 304            }
 142305            else if (!item.IsFolder)
 306            {
 0307                if (item is not Video && item is not LiveTvChannel)
 308                {
 0309                    return;
 310                }
 311            }
 312
 142313            _cache.AddOrUpdate(item.Id, item);
 142314        }
 315
 316        public void DeleteItem(BaseItem item, DeleteOptions options)
 317        {
 0318            DeleteItem(item, options, false);
 0319        }
 320
 321        public void DeleteItem(BaseItem item, DeleteOptions options, bool notifyParentItem)
 322        {
 0323            ArgumentNullException.ThrowIfNull(item);
 324
 0325            var parent = item.GetOwner() ?? item.GetParent();
 326
 0327            DeleteItem(item, options, parent, notifyParentItem);
 0328        }
 329
 330        public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
 331        {
 1332            ArgumentNullException.ThrowIfNull(item);
 333
 1334            if (item.SourceType == SourceType.Channel)
 335            {
 0336                if (options.DeleteFromExternalProvider)
 337                {
 338                    try
 339                    {
 0340                        BaseItem.ChannelManager.DeleteItem(item).GetAwaiter().GetResult();
 0341                    }
 0342                    catch (ArgumentException)
 343                    {
 344                        // channel no longer installed
 0345                    }
 346                }
 347
 0348                options.DeleteFileLocation = false;
 349            }
 350
 1351            if (item is LiveTvProgram)
 352            {
 0353                _logger.LogDebug(
 0354                    "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0355                    item.GetType().Name,
 0356                    item.Name ?? "Unknown name",
 0357                    item.Path ?? string.Empty,
 0358                    item.Id);
 359            }
 360            else
 361            {
 1362                _logger.LogInformation(
 1363                    "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 1364                    item.GetType().Name,
 1365                    item.Name ?? "Unknown name",
 1366                    item.Path ?? string.Empty,
 1367                    item.Id);
 368            }
 369
 1370            var children = item.IsFolder
 1371                ? ((Folder)item).GetRecursiveChildren(false)
 1372                : [];
 373
 4374            foreach (var metadataPath in GetMetadataPaths(item, children))
 375            {
 1376                if (!Directory.Exists(metadataPath))
 377                {
 378                    continue;
 379                }
 380
 0381                _logger.LogDebug(
 0382                    "Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0383                    item.GetType().Name,
 0384                    item.Name ?? "Unknown name",
 0385                    metadataPath,
 0386                    item.Id);
 387
 388                try
 389                {
 0390                    Directory.Delete(metadataPath, true);
 0391                }
 0392                catch (Exception ex)
 393                {
 0394                    _logger.LogError(ex, "Error deleting {MetadataPath}", metadataPath);
 0395                }
 396            }
 397
 1398            if ((options.DeleteFileLocation && item.IsFileProtocol) || IsInternalItem(item))
 399            {
 400                // Assume only the first is required
 401                // Add this flag to GetDeletePaths if required in the future
 0402                var isRequiredForDelete = true;
 403
 0404                foreach (var fileSystemInfo in item.GetDeletePaths())
 405                {
 0406                    if (Directory.Exists(fileSystemInfo.FullName) || File.Exists(fileSystemInfo.FullName))
 407                    {
 408                        try
 409                        {
 0410                            _logger.LogInformation(
 0411                                "Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0412                                item.GetType().Name,
 0413                                item.Name ?? "Unknown name",
 0414                                fileSystemInfo.FullName,
 0415                                item.Id);
 416
 0417                            if (fileSystemInfo.IsDirectory)
 418                            {
 0419                                Directory.Delete(fileSystemInfo.FullName, true);
 420                            }
 421                            else
 422                            {
 0423                                File.Delete(fileSystemInfo.FullName);
 424                            }
 0425                        }
 0426                        catch (DirectoryNotFoundException)
 427                        {
 0428                            _logger.LogInformation(
 0429                                "Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Pa
 0430                                item.GetType().Name,
 0431                                item.Name ?? "Unknown name",
 0432                                fileSystemInfo.FullName,
 0433                                item.Id);
 0434                        }
 0435                        catch (FileNotFoundException)
 436                        {
 0437                            _logger.LogInformation(
 0438                                "File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, 
 0439                                item.GetType().Name,
 0440                                item.Name ?? "Unknown name",
 0441                                fileSystemInfo.FullName,
 0442                                item.Id);
 0443                        }
 0444                        catch (IOException)
 445                        {
 0446                            if (isRequiredForDelete)
 447                            {
 0448                                throw;
 449                            }
 0450                        }
 0451                        catch (UnauthorizedAccessException)
 452                        {
 0453                            if (isRequiredForDelete)
 454                            {
 0455                                throw;
 456                            }
 0457                        }
 458                    }
 459
 0460                    isRequiredForDelete = false;
 461                }
 462            }
 463
 1464            item.SetParent(null);
 465
 1466            _itemRepository.DeleteItem(item.Id);
 1467            _cache.TryRemove(item.Id, out _);
 2468            foreach (var child in children)
 469            {
 0470                _itemRepository.DeleteItem(child.Id);
 0471                _cache.TryRemove(child.Id, out _);
 472            }
 473
 1474            ReportItemRemoved(item, parent);
 1475        }
 476
 477        private bool IsInternalItem(BaseItem item)
 478        {
 1479            if (!item.IsFileProtocol)
 480            {
 0481                return false;
 482            }
 483
 1484            var pathToCheck = item switch
 1485            {
 0486                Genre => _configurationManager.ApplicationPaths.GenrePath,
 0487                MusicArtist => _configurationManager.ApplicationPaths.ArtistsPath,
 0488                MusicGenre => _configurationManager.ApplicationPaths.GenrePath,
 0489                Person => _configurationManager.ApplicationPaths.PeoplePath,
 0490                Studio => _configurationManager.ApplicationPaths.StudioPath,
 0491                Year => _configurationManager.ApplicationPaths.YearPath,
 1492                _ => null
 1493            };
 494
 1495            var itemPath = item.Path;
 1496            if (!string.IsNullOrEmpty(pathToCheck) && !string.IsNullOrEmpty(itemPath))
 497            {
 0498                var cleanPath = _fileSystem.GetValidFilename(itemPath);
 0499                var cleanCheckPath = _fileSystem.GetValidFilename(pathToCheck);
 500
 0501                return cleanPath.StartsWith(cleanCheckPath, StringComparison.Ordinal);
 502            }
 503
 1504            return false;
 505        }
 506
 507        private List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
 508        {
 1509            var list = GetInternalMetadataPaths(item);
 2510            foreach (var child in children)
 511            {
 0512                list.AddRange(GetInternalMetadataPaths(child));
 513            }
 514
 1515            return list;
 516        }
 517
 518        private List<string> GetInternalMetadataPaths(BaseItem item)
 519        {
 1520            var list = new List<string>
 1521            {
 1522                item.GetInternalMetadataPath()
 1523            };
 524
 1525            if (item is Video video)
 526            {
 527                // Trickplay
 0528                list.Add(_pathManager.GetTrickplayDirectory(video));
 529
 530                // Subtitles and attachments
 0531                foreach (var mediaSource in item.GetMediaSources(false))
 532                {
 0533                    var subtitleFolder = _pathManager.GetSubtitleFolderPath(mediaSource.Id);
 0534                    if (subtitleFolder is not null)
 535                    {
 0536                        list.Add(subtitleFolder);
 537                    }
 538
 0539                    var attachmentFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
 0540                    if (attachmentFolder is not null)
 541                    {
 0542                        list.Add(attachmentFolder);
 543                    }
 544                }
 545            }
 546
 1547            return list;
 548        }
 549
 550        /// <summary>
 551        /// Resolves the item.
 552        /// </summary>
 553        /// <param name="args">The args.</param>
 554        /// <param name="resolvers">The resolvers.</param>
 555        /// <returns>BaseItem.</returns>
 556        private BaseItem? ResolveItem(ItemResolveArgs args, IItemResolver[]? resolvers)
 557        {
 93558            var item = (resolvers ?? EntityResolvers).Select(r => Resolve(args, r))
 93559                .FirstOrDefault(i => i is not null);
 560
 93561            if (item is not null)
 562            {
 83563                ResolverHelper.SetInitialItemValues(item, args, _fileSystem, this);
 564            }
 565
 93566            return item;
 567        }
 568
 569        private BaseItem? Resolve(ItemResolveArgs args, IItemResolver resolver)
 570        {
 571            try
 572            {
 355573                return resolver.ResolvePath(args);
 574            }
 0575            catch (Exception ex)
 576            {
 0577                _logger.LogError(ex, "Error in {Resolver} resolving {Path}", resolver.GetType().Name, args.Path);
 0578                return null;
 579            }
 355580        }
 581
 582        public Guid GetNewItemId(string key, Type type)
 583        {
 180584            return GetNewItemIdInternal(key, type, false);
 585        }
 586
 587        private Guid GetNewItemIdInternal(string key, Type type, bool forceCaseInsensitive)
 588        {
 181589            ArgumentException.ThrowIfNullOrEmpty(key);
 181590            ArgumentNullException.ThrowIfNull(type);
 591
 181592            string programDataPath = _configurationManager.ApplicationPaths.ProgramDataPath;
 181593            if (key.StartsWith(programDataPath, StringComparison.Ordinal))
 594            {
 595                // Try to normalize paths located underneath program-data in an attempt to make them more portable
 164596                key = key.Substring(programDataPath.Length)
 164597                    .TrimStart('/', '\\')
 164598                    .Replace('/', '\\');
 599            }
 600
 181601            if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
 602            {
 1603                key = key.ToLowerInvariant();
 604            }
 605
 181606            key = type.FullName + key;
 607
 181608            return key.GetMD5();
 609        }
 610
 611        public BaseItem? ResolvePath(FileSystemMetadata fileInfo, Folder? parent = null, IDirectoryService? directorySer
 59612            => ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent);
 613
 614        private BaseItem? ResolvePath(
 615            FileSystemMetadata fileInfo,
 616            IDirectoryService directoryService,
 617            IItemResolver[]? resolvers,
 618            Folder? parent = null,
 619            CollectionType? collectionType = null,
 620            LibraryOptions? libraryOptions = null)
 621        {
 93622            ArgumentNullException.ThrowIfNull(fileInfo);
 623
 93624            var fullPath = fileInfo.FullName;
 625
 93626            if (collectionType is null && parent is not null)
 627            {
 17628                collectionType = GetContentTypeOverride(fullPath, true);
 629            }
 630
 93631            var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, this)
 93632            {
 93633                Parent = parent,
 93634                FileInfo = fileInfo,
 93635                CollectionType = collectionType,
 93636                LibraryOptions = libraryOptions
 93637            };
 638
 639            // Return null if ignore rules deem that we should do so
 93640            if (IgnoreFile(args.FileInfo, args.Parent))
 641            {
 0642                return null;
 643            }
 644
 645            // Gather child folder and files
 93646            if (args.IsDirectory)
 647            {
 66648                var isPhysicalRoot = args.IsPhysicalRoot;
 649
 650                // When resolving the root, we need it's grandchildren (children of user views)
 66651                var flattenFolderDepth = isPhysicalRoot ? 2 : 0;
 652
 653                FileSystemMetadata[] files;
 66654                var isVf = args.IsVf;
 655
 656                try
 657                {
 66658                    files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, _fileSystem, _appHost, _l
 66659                }
 0660                catch (Exception ex)
 661                {
 0662                    if (parent is not null && parent.IsPhysicalRoot)
 663                    {
 0664                        _logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPh
 665
 0666                        files = [];
 667                    }
 668                    else
 669                    {
 0670                        throw;
 671                    }
 0672                }
 673
 674                // Need to remove sub-paths that may have been resolved from shortcuts
 675                // Example: if \\server\movies exists, then strip out \\server\movies\action
 66676                if (isPhysicalRoot)
 677                {
 38678                    files = NormalizeRootPathList(files).ToArray();
 679                }
 680
 66681                args.FileSystemChildren = files;
 682            }
 683
 684            // Filter content based on ignore rules
 93685            if (args.IsDirectory)
 686            {
 66687                var filtered = args.GetActualFileSystemChildren().ToArray();
 66688                args.FileSystemChildren = filtered ?? [];
 689            }
 690
 93691            return ResolveItem(args, resolvers);
 692        }
 693
 694        public bool IgnoreFile(FileSystemMetadata file, BaseItem? parent)
 117695            => EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(file, parent));
 696
 697        public List<FileSystemMetadata> NormalizeRootPathList(IEnumerable<FileSystemMetadata> paths)
 698        {
 87699            var originalList = paths.ToList();
 700
 87701            var list = originalList.Where(i => i.IsDirectory)
 87702                .Select(i => Path.TrimEndingDirectorySeparator(i.FullName))
 87703                .Distinct()
 87704                .ToList();
 705
 87706            var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.Ordinal) && list.Any(i => _fileS
 87707                .ToList();
 708
 174709            foreach (var dupe in dupes)
 710            {
 0711                _logger.LogInformation("Found duplicate path: {0}", dupe);
 712            }
 713
 87714            var newList = list.Except(dupes, StringComparer.Ordinal).Select(_fileSystem.GetDirectoryInfo).ToList();
 87715            newList.AddRange(originalList.Where(i => !i.IsDirectory));
 87716            return newList;
 717        }
 718
 719        public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryServ
 720        {
 42721            return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers);
 722        }
 723
 724        public IEnumerable<BaseItem> ResolvePaths(
 725            IEnumerable<FileSystemMetadata> files,
 726            IDirectoryService directoryService,
 727            Folder parent,
 728            LibraryOptions libraryOptions,
 729            CollectionType? collectionType,
 730            IItemResolver[] resolvers)
 731        {
 42732            var fileList = files.Where(i => !IgnoreFile(i, parent)).ToList();
 733
 42734            if (parent is not null)
 735            {
 42736                var multiItemResolvers = resolvers is null ? MultiItemResolvers : resolvers.OfType<IMultiItemResolver>()
 737
 252738                foreach (var resolver in multiItemResolvers)
 739                {
 84740                    var result = resolver.ResolveMultiple(parent, fileList, collectionType, directoryService);
 741
 84742                    if (result?.Items.Count > 0)
 743                    {
 0744                        var items = result.Items;
 0745                        items.RemoveAll(item => !ResolverHelper.SetInitialItemValues(item, parent, this, directoryServic
 0746                        items.AddRange(ResolveFileList(result.ExtraFiles, directoryService, parent, collectionType, reso
 0747                        return items;
 748                    }
 749                }
 750            }
 751
 42752            return ResolveFileList(fileList, directoryService, parent, collectionType, resolvers, libraryOptions);
 0753        }
 754
 755        private IEnumerable<BaseItem> ResolveFileList(
 756            IReadOnlyList<FileSystemMetadata> fileList,
 757            IDirectoryService directoryService,
 758            Folder? parent,
 759            CollectionType? collectionType,
 760            IItemResolver[]? resolvers,
 761            LibraryOptions libraryOptions)
 762        {
 763            // Given that fileList is a list we can save enumerator allocations by indexing
 764            for (var i = 0; i < fileList.Count; i++)
 765            {
 766                var file = fileList[i];
 767                BaseItem? result = null;
 768                try
 769                {
 770                    result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions);
 771                }
 772                catch (Exception ex)
 773                {
 774                    _logger.LogError(ex, "Error resolving path {Path}", file.FullName);
 775                }
 776
 777                if (result is not null)
 778                {
 779                    yield return result;
 780                }
 781            }
 782        }
 783
 784        /// <summary>
 785        /// Creates the root media folder.
 786        /// </summary>
 787        /// <returns>AggregateFolder.</returns>
 788        /// <exception cref="InvalidOperationException">Cannot create the root folder until plugins have loaded.</except
 789        public AggregateFolder CreateRootFolder()
 790        {
 38791            var rootFolderPath = _configurationManager.ApplicationPaths.RootFolderPath;
 792
 38793            var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ??
 38794                             (ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)) as Folder ?? throw new InvalidOp
 38795                             .DeepCopy<Folder, AggregateFolder>();
 796
 797            // In case program data folder was moved
 38798            if (!string.Equals(rootFolder.Path, rootFolderPath, StringComparison.Ordinal))
 799            {
 0800                _logger.LogInformation("Resetting root folder path to {0}", rootFolderPath);
 0801                rootFolder.Path = rootFolderPath;
 802            }
 803
 804            // Add in the plug-in folders
 38805            var path = Path.Combine(_configurationManager.ApplicationPaths.DataPath, "playlists");
 806
 38807            var info = Directory.CreateDirectory(path);
 38808            Folder folder = new PlaylistsFolder
 38809            {
 38810                Path = path,
 38811                DateCreated = info.CreationTimeUtc,
 38812                DateModified = info.LastWriteTimeUtc,
 38813            };
 814
 38815            if (folder.Id.IsEmpty())
 816            {
 38817                folder.Id = GetNewItemId(folder.Path, folder.GetType());
 818            }
 819
 38820            var dbItem = GetItemById(folder.Id) as BasePluginFolder;
 821
 38822            if (dbItem is not null && string.Equals(dbItem.Path, folder.Path, StringComparison.OrdinalIgnoreCase))
 823            {
 17824                folder = dbItem;
 825            }
 826
 38827            if (!folder.ParentId.Equals(rootFolder.Id))
 828            {
 21829                folder.ParentId = rootFolder.Id;
 21830                folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetRe
 831            }
 832
 17833            rootFolder.AddVirtualChild(folder);
 834
 17835            RegisterItem(folder);
 836
 17837            return rootFolder;
 838        }
 839
 840        public Folder GetUserRootFolder()
 841        {
 739842            if (_userRootFolder is null)
 21843            {
 844                lock (_userRootFolderSyncLock)
 845                {
 21846                    if (_userRootFolder is null)
 847                    {
 21848                        var userRootPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 849
 21850                        _logger.LogDebug("Creating userRootPath at {Path}", userRootPath);
 21851                        Directory.CreateDirectory(userRootPath);
 852
 21853                        var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder));
 21854                        UserRootFolder? tmpItem = null;
 855                        try
 856                        {
 21857                            tmpItem = GetItemById(newItemId) as UserRootFolder;
 21858                        }
 0859                        catch (Exception ex)
 860                        {
 0861                            _logger.LogError(ex, "Error creating UserRootFolder {Path}", newItemId);
 0862                        }
 863
 21864                        if (tmpItem is null)
 865                        {
 21866                            _logger.LogDebug("Creating new userRootFolder with DeepCopy");
 21867                            tmpItem = (ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath)) as Folder ?? throw new In
 21868                                        .DeepCopy<Folder, UserRootFolder>();
 869                        }
 870
 871                        // In case program data folder was moved
 21872                        if (!string.Equals(tmpItem.Path, userRootPath, StringComparison.Ordinal))
 873                        {
 0874                            _logger.LogInformation("Resetting user root folder path to {0}", userRootPath);
 0875                            tmpItem.Path = userRootPath;
 876                        }
 877
 21878                        _userRootFolder = tmpItem;
 21879                        _logger.LogDebug("Setting userRootFolder: {Folder}", _userRootFolder);
 880                    }
 21881                }
 882            }
 883
 739884            return _userRootFolder;
 885        }
 886
 887        /// <inheritdoc />
 888        public BaseItem? FindByPath(string path, bool? isFolder)
 889        {
 890            // If this returns multiple items it could be tricky figuring out which one is correct.
 891            // In most cases, the newest one will be and the others obsolete but not yet cleaned up
 0892            ArgumentException.ThrowIfNullOrEmpty(path);
 893
 0894            var query = new InternalItemsQuery
 0895            {
 0896                Path = path,
 0897                IsFolder = isFolder,
 0898                OrderBy = [(ItemSortBy.DateCreated, SortOrder.Descending)],
 0899                Limit = 1,
 0900                DtoOptions = new DtoOptions(true)
 0901            };
 902
 0903            return GetItemList(query)
 0904                .FirstOrDefault();
 905        }
 906
 907        /// <inheritdoc />
 908        public Person? GetPerson(string name)
 909        {
 1910            var path = Person.GetPath(name);
 1911            var id = GetItemByNameId<Person>(path);
 1912            if (GetItemById(id) is Person item)
 913            {
 0914                return item;
 915            }
 916
 1917            return null;
 918        }
 919
 920        /// <summary>
 921        /// Gets the studio.
 922        /// </summary>
 923        /// <param name="name">The name.</param>
 924        /// <returns>Task{Studio}.</returns>
 925        public Studio GetStudio(string name)
 926        {
 0927            return CreateItemByName<Studio>(Studio.GetPath, name, new DtoOptions(true));
 928        }
 929
 930        public Guid GetStudioId(string name)
 931        {
 0932            return GetItemByNameId<Studio>(Studio.GetPath(name));
 933        }
 934
 935        public Guid GetGenreId(string name)
 936        {
 0937            return GetItemByNameId<Genre>(Genre.GetPath(name));
 938        }
 939
 940        public Guid GetMusicGenreId(string name)
 941        {
 0942            return GetItemByNameId<MusicGenre>(MusicGenre.GetPath(name));
 943        }
 944
 945        /// <summary>
 946        /// Gets the genre.
 947        /// </summary>
 948        /// <param name="name">The name.</param>
 949        /// <returns>Task{Genre}.</returns>
 950        public Genre GetGenre(string name)
 951        {
 0952            return CreateItemByName<Genre>(Genre.GetPath, name, new DtoOptions(true));
 953        }
 954
 955        /// <summary>
 956        /// Gets the music genre.
 957        /// </summary>
 958        /// <param name="name">The name.</param>
 959        /// <returns>Task{MusicGenre}.</returns>
 960        public MusicGenre GetMusicGenre(string name)
 961        {
 0962            return CreateItemByName<MusicGenre>(MusicGenre.GetPath, name, new DtoOptions(true));
 963        }
 964
 965        /// <summary>
 966        /// Gets the year.
 967        /// </summary>
 968        /// <param name="value">The value.</param>
 969        /// <returns>Task{Year}.</returns>
 970        public Year GetYear(int value)
 971        {
 0972            if (value <= 0)
 973            {
 0974                throw new ArgumentOutOfRangeException(nameof(value), "Years less than or equal to 0 are invalid.");
 975            }
 976
 0977            var name = value.ToString(CultureInfo.InvariantCulture);
 978
 0979            return CreateItemByName<Year>(Year.GetPath, name, new DtoOptions(true));
 980        }
 981
 982        /// <summary>
 983        /// Gets a Genre.
 984        /// </summary>
 985        /// <param name="name">The name.</param>
 986        /// <returns>Task{Genre}.</returns>
 987        public MusicArtist GetArtist(string name)
 988        {
 0989            return GetArtist(name, new DtoOptions(true));
 990        }
 991
 992        public MusicArtist GetArtist(string name, DtoOptions options)
 993        {
 0994            return CreateItemByName<MusicArtist>(MusicArtist.GetPath, name, options);
 995        }
 996
 997        private T CreateItemByName<T>(Func<string, string> getPathFn, string name, DtoOptions options)
 998            where T : BaseItem, new()
 999        {
 01000            if (typeof(T) == typeof(MusicArtist))
 1001            {
 01002                var existing = GetItemList(new InternalItemsQuery
 01003                {
 01004                    IncludeItemTypes = [BaseItemKind.MusicArtist],
 01005                    Name = name,
 01006                    DtoOptions = options
 01007                }).Cast<MusicArtist>()
 01008                .OrderBy(i => i.IsAccessedByName ? 1 : 0)
 01009                .Cast<T>()
 01010                .FirstOrDefault();
 1011
 01012                if (existing is not null)
 1013                {
 01014                    return existing;
 1015                }
 1016            }
 1017
 01018            var path = getPathFn(name);
 01019            var id = GetItemByNameId<T>(path);
 01020            var item = GetItemById(id) as T;
 01021            if (item is null)
 1022            {
 01023                var info = Directory.CreateDirectory(path);
 01024                item = new T
 01025                {
 01026                    Name = name,
 01027                    Id = id,
 01028                    DateCreated = info.CreationTimeUtc,
 01029                    DateModified = info.LastWriteTimeUtc,
 01030                    Path = path
 01031                };
 1032
 01033                CreateItem(item, null);
 1034            }
 1035
 01036            return item;
 1037        }
 1038
 1039        private Guid GetItemByNameId<T>(string path)
 1040              where T : BaseItem, new()
 1041        {
 11042            var forceCaseInsensitiveId = _configurationManager.Configuration.EnableNormalizedItemByNameIds;
 11043            return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId);
 1044        }
 1045
 1046        /// <inheritdoc />
 1047        public Task ValidatePeopleAsync(IProgress<double> progress, CancellationToken cancellationToken)
 1048        {
 1049            // Ensure the location is available.
 01050            Directory.CreateDirectory(_configurationManager.ApplicationPaths.PeoplePath);
 1051
 01052            return new PeopleValidator(this, _logger, _fileSystem).ValidatePeople(cancellationToken, progress);
 1053        }
 1054
 1055        /// <summary>
 1056        /// Reloads the root media folder.
 1057        /// </summary>
 1058        /// <param name="progress">The progress.</param>
 1059        /// <param name="cancellationToken">The cancellation token.</param>
 1060        /// <returns>Task.</returns>
 1061        public Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken)
 1062        {
 1063            // Just run the scheduled task so that the user can see it
 31064            _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
 1065
 31066            return Task.CompletedTask;
 1067        }
 1068
 1069        /// <summary>
 1070        /// Validates the media library internal.
 1071        /// </summary>
 1072        /// <param name="progress">The progress.</param>
 1073        /// <param name="cancellationToken">The cancellation token.</param>
 1074        /// <returns>Task.</returns>
 1075        public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken)
 1076        {
 1077            IsScanRunning = true;
 1078            LibraryMonitor.Stop();
 1079
 1080            try
 1081            {
 1082                await PerformLibraryValidation(progress, cancellationToken).ConfigureAwait(false);
 1083            }
 1084            finally
 1085            {
 1086                LibraryMonitor.Start();
 1087                IsScanRunning = false;
 1088            }
 1089        }
 1090
 1091        public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
 1092        {
 1093            RootFolder.Children = null;
 1094            await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1095
 1096            // Start by just validating the children of the root, but go no further
 1097            await RootFolder.ValidateChildren(
 1098                new Progress<double>(),
 1099                new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
 1100                recursive: false,
 1101                allowRemoveRoot: removeRoot,
 1102                cancellationToken: cancellationToken).ConfigureAwait(false);
 1103
 1104            var rootFolder = GetUserRootFolder();
 1105            rootFolder.Children = null;
 1106
 1107            await rootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1108
 1109            await rootFolder.ValidateChildren(
 1110                new Progress<double>(),
 1111                new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
 1112                recursive: false,
 1113                allowRemoveRoot: removeRoot,
 1114                cancellationToken: cancellationToken).ConfigureAwait(false);
 1115
 1116            // Quickly scan CollectionFolders for changes
 1117            foreach (var child in rootFolder.Children!.OfType<Folder>())
 1118            {
 1119                // If the user has somehow deleted the collection directory, remove the metadata from the database.
 1120                if (child is CollectionFolder collectionFolder && !Directory.Exists(collectionFolder.Path))
 1121                {
 1122                    _itemRepository.DeleteItem(collectionFolder.Id);
 1123                }
 1124                else
 1125                {
 1126                    await child.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1127                }
 1128            }
 1129        }
 1130
 1131        private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken)
 1132        {
 1133            _logger.LogInformation("Validating media library");
 1134
 1135            await ValidateTopLibraryFolders(cancellationToken).ConfigureAwait(false);
 1136
 1137            var innerProgress = new Progress<double>(pct => progress.Report(pct * 0.96));
 1138
 1139            // Validate the entire media library
 1140            await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem
 1141
 1142            progress.Report(96);
 1143
 1144            innerProgress = new Progress<double>(pct => progress.Report(96 + (pct * .04)));
 1145
 1146            await RunPostScanTasks(innerProgress, cancellationToken).ConfigureAwait(false);
 1147
 1148            progress.Report(100);
 1149        }
 1150
 1151        /// <summary>
 1152        /// Runs the post scan tasks.
 1153        /// </summary>
 1154        /// <param name="progress">The progress.</param>
 1155        /// <param name="cancellationToken">The cancellation token.</param>
 1156        /// <returns>Task.</returns>
 1157        private async Task RunPostScanTasks(IProgress<double> progress, CancellationToken cancellationToken)
 1158        {
 1159            var tasks = PostScanTasks.ToList();
 1160
 1161            var numComplete = 0;
 1162            var numTasks = tasks.Count;
 1163
 1164            foreach (var task in tasks)
 1165            {
 1166                // Prevent access to modified closure
 1167                var currentNumComplete = numComplete;
 1168
 1169                var innerProgress = new Progress<double>(pct =>
 1170                {
 1171                    double innerPercent = pct;
 1172                    innerPercent /= 100;
 1173                    innerPercent += currentNumComplete;
 1174
 1175                    innerPercent /= numTasks;
 1176                    innerPercent *= 100;
 1177
 1178                    progress.Report(innerPercent);
 1179                });
 1180
 1181                _logger.LogDebug("Running post-scan task {0}", task.GetType().Name);
 1182
 1183                try
 1184                {
 1185                    await task.Run(innerProgress, cancellationToken).ConfigureAwait(false);
 1186                }
 1187                catch (OperationCanceledException)
 1188                {
 1189                    _logger.LogInformation("Post-scan task cancelled: {0}", task.GetType().Name);
 1190                    throw;
 1191                }
 1192                catch (Exception ex)
 1193                {
 1194                    _logger.LogError(ex, "Error running post-scan task");
 1195                }
 1196
 1197                numComplete++;
 1198                double percent = numComplete;
 1199                percent /= numTasks;
 1200                progress.Report(percent * 100);
 1201            }
 1202
 1203            _itemRepository.UpdateInheritedValues();
 1204
 1205            progress.Report(100);
 1206        }
 1207
 1208        /// <summary>
 1209        /// Gets the default view.
 1210        /// </summary>
 1211        /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
 1212        public List<VirtualFolderInfo> GetVirtualFolders()
 1213        {
 231214            return GetVirtualFolders(false);
 1215        }
 1216
 1217        public List<VirtualFolderInfo> GetVirtualFolders(bool includeRefreshState)
 1218        {
 241219            _logger.LogDebug("Getting topLibraryFolders");
 241220            var topLibraryFolders = GetUserRootFolder().Children.ToList();
 1221
 241222            _logger.LogDebug("Getting refreshQueue");
 241223            var refreshQueue = includeRefreshState ? ProviderManager.GetRefreshQueue() : null;
 1224
 241225            return _fileSystem.GetDirectoryPaths(_configurationManager.ApplicationPaths.DefaultUserViewsPath)
 241226                .Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders, refreshQueue))
 241227                .ToList();
 1228        }
 1229
 1230        private VirtualFolderInfo GetVirtualFolderInfo(string dir, List<BaseItem> allCollectionFolders, HashSet<Guid>? r
 1231        {
 11232            var info = new VirtualFolderInfo
 11233            {
 11234                Name = Path.GetFileName(dir),
 11235
 11236                Locations = _fileSystem.GetFilePaths(dir, false)
 11237                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCa
 11238                    .Select(i =>
 11239                    {
 11240                        try
 11241                        {
 11242                            return _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(i));
 11243                        }
 11244                        catch (Exception ex)
 11245                        {
 11246                            _logger.LogError(ex, "Error resolving shortcut file {File}", i);
 11247                            return null;
 11248                        }
 11249                    })
 11250                    .Where(i => i is not null)
 11251                    .Order()
 11252                    .ToArray(),
 11253
 11254                CollectionType = GetCollectionType(dir)
 11255            };
 1256
 11257            var libraryFolder = allCollectionFolders.FirstOrDefault(i => string.Equals(i.Path, dir, StringComparison.Ord
 11258            if (libraryFolder is not null)
 1259            {
 11260                var libraryFolderId = libraryFolder.Id.ToString("N", CultureInfo.InvariantCulture);
 11261                info.ItemId = libraryFolderId;
 11262                if (libraryFolder.HasImage(ImageType.Primary))
 1263                {
 01264                    info.PrimaryImageItemId = libraryFolderId;
 1265                }
 1266
 11267                info.LibraryOptions = GetLibraryOptions(libraryFolder);
 1268
 11269                if (refreshQueue is not null)
 1270                {
 11271                    info.RefreshProgress = libraryFolder.GetRefreshProgress();
 1272
 11273                    info.RefreshStatus = info.RefreshProgress.HasValue ? "Active" : refreshQueue.Contains(libraryFolder.
 1274                }
 1275            }
 1276
 11277            return info;
 1278        }
 1279
 1280        private CollectionTypeOptions? GetCollectionType(string path)
 1281        {
 11282            var files = _fileSystem.GetFilePaths(path, [".collection"], true, false);
 21283            foreach (ReadOnlySpan<char> file in files)
 1284            {
 01285                if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
 1286                {
 01287                    return res;
 1288                }
 1289            }
 1290
 11291            return null;
 01292        }
 1293
 1294        /// <inheritdoc />
 1295        public BaseItem? GetItemById(Guid id)
 1296        {
 5411297            if (id.IsEmpty())
 1298            {
 01299                throw new ArgumentException("Guid can't be empty", nameof(id));
 1300            }
 1301
 5411302            if (_cache.TryGet(id, out var item))
 1303            {
 2951304                return item;
 1305            }
 1306
 2461307            item = RetrieveItem(id);
 1308
 2461309            if (item is not null)
 1310            {
 01311                RegisterItem(item);
 1312            }
 1313
 2461314            return item;
 1315        }
 1316
 1317        /// <inheritdoc />
 1318        public T? GetItemById<T>(Guid id)
 1319            where T : BaseItem
 1320        {
 231321            var item = GetItemById(id);
 231322            if (item is T typedItem)
 1323            {
 11324                return typedItem;
 1325            }
 1326
 221327            return null;
 1328        }
 1329
 1330        /// <inheritdoc />
 1331        public T? GetItemById<T>(Guid id, Guid userId)
 1332            where T : BaseItem
 1333        {
 11334            var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
 11335            return GetItemById<T>(id, user);
 1336        }
 1337
 1338        /// <inheritdoc />
 1339        public T? GetItemById<T>(Guid id, User? user)
 1340            where T : BaseItem
 1341        {
 211342            var item = GetItemById<T>(id);
 211343            return ItemIsVisible(item, user) ? item : null;
 1344        }
 1345
 1346        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
 1347        {
 971348            if (query.Recursive && !query.ParentId.IsEmpty())
 1349            {
 401350                var parent = GetItemById(query.ParentId);
 401351                if (parent is not null)
 1352                {
 401353                    SetTopParentIdsOrAncestors(query, [parent]);
 1354                }
 1355            }
 1356
 971357            if (query.User is not null)
 1358            {
 11359                AddUserToQuery(query, query.User, allowExternalContent);
 1360            }
 1361
 971362            var itemList = _itemRepository.GetItemList(query);
 971363            var user = query.User;
 971364            if (user is not null)
 1365            {
 11366                return itemList.Where(i => i.IsVisible(user)).ToList();
 1367            }
 1368
 961369            return itemList;
 1370        }
 1371
 1372        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
 1373        {
 971374            return GetItemList(query, true);
 1375        }
 1376
 1377        public int GetCount(InternalItemsQuery query)
 1378        {
 01379            if (query.Recursive && !query.ParentId.IsEmpty())
 1380            {
 01381                var parent = GetItemById(query.ParentId);
 01382                if (parent is not null)
 1383                {
 01384                    SetTopParentIdsOrAncestors(query, [parent]);
 1385                }
 1386            }
 1387
 01388            if (query.User is not null)
 1389            {
 01390                AddUserToQuery(query, query.User);
 1391            }
 1392
 01393            return _itemRepository.GetCount(query);
 1394        }
 1395
 1396        public ItemCounts GetItemCounts(InternalItemsQuery query)
 1397        {
 01398            if (query.Recursive && !query.ParentId.IsEmpty())
 1399            {
 01400                var parent = GetItemById(query.ParentId);
 01401                if (parent is not null)
 1402                {
 01403                    SetTopParentIdsOrAncestors(query, [parent]);
 1404                }
 1405            }
 1406
 01407            if (query.User is not null)
 1408            {
 01409                AddUserToQuery(query, query.User);
 1410            }
 1411
 01412            return _itemRepository.GetItemCounts(query);
 1413        }
 1414
 1415        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
 1416        {
 01417            SetTopParentIdsOrAncestors(query, parents);
 1418
 01419            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1420            {
 01421                if (query.User is not null)
 1422                {
 01423                    AddUserToQuery(query, query.User);
 1424                }
 1425            }
 1426
 01427            return _itemRepository.GetItemList(query);
 1428        }
 1429
 1430        public IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery query, IReadOnlyList<BaseItem> parents, Coll
 1431        {
 01432            SetTopParentIdsOrAncestors(query, parents);
 1433
 01434            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1435            {
 01436                if (query.User is not null)
 1437                {
 01438                    AddUserToQuery(query, query.User);
 1439                }
 1440            }
 1441
 01442            return _itemRepository.GetLatestItemList(query, collectionType);
 1443        }
 1444
 1445        public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents
 1446        {
 01447            SetTopParentIdsOrAncestors(query, parents);
 1448
 01449            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1450            {
 01451                if (query.User is not null)
 1452                {
 01453                    AddUserToQuery(query, query.User);
 1454                }
 1455            }
 1456
 01457            return _itemRepository.GetNextUpSeriesKeys(query, dateCutoff);
 1458        }
 1459
 1460        public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
 1461        {
 01462            if (query.User is not null)
 1463            {
 01464                AddUserToQuery(query, query.User);
 1465            }
 1466
 01467            if (query.EnableTotalRecordCount)
 1468            {
 01469                return _itemRepository.GetItems(query);
 1470            }
 1471
 01472            return new QueryResult<BaseItem>(
 01473                query.StartIndex,
 01474                null,
 01475                _itemRepository.GetItemList(query));
 1476        }
 1477
 1478        public IReadOnlyList<Guid> GetItemIds(InternalItemsQuery query)
 1479        {
 111480            if (query.User is not null)
 1481            {
 01482                AddUserToQuery(query, query.User);
 1483            }
 1484
 111485            return _itemRepository.GetItemIdsList(query);
 1486        }
 1487
 1488        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query)
 1489        {
 01490            if (query.User is not null)
 1491            {
 01492                AddUserToQuery(query, query.User);
 1493            }
 1494
 01495            SetTopParentOrAncestorIds(query);
 01496            return _itemRepository.GetStudios(query);
 1497        }
 1498
 1499        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query)
 1500        {
 01501            if (query.User is not null)
 1502            {
 01503                AddUserToQuery(query, query.User);
 1504            }
 1505
 01506            SetTopParentOrAncestorIds(query);
 01507            return _itemRepository.GetGenres(query);
 1508        }
 1509
 1510        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query)
 1511        {
 01512            if (query.User is not null)
 1513            {
 01514                AddUserToQuery(query, query.User);
 1515            }
 1516
 01517            SetTopParentOrAncestorIds(query);
 01518            return _itemRepository.GetMusicGenres(query);
 1519        }
 1520
 1521        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query)
 1522        {
 01523            if (query.User is not null)
 1524            {
 01525                AddUserToQuery(query, query.User);
 1526            }
 1527
 01528            SetTopParentOrAncestorIds(query);
 01529            return _itemRepository.GetAllArtists(query);
 1530        }
 1531
 1532        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query)
 1533        {
 01534            if (query.User is not null)
 1535            {
 01536                AddUserToQuery(query, query.User);
 1537            }
 1538
 01539            SetTopParentOrAncestorIds(query);
 01540            return _itemRepository.GetArtists(query);
 1541        }
 1542
 1543        private void SetTopParentOrAncestorIds(InternalItemsQuery query)
 1544        {
 01545            var ancestorIds = query.AncestorIds;
 01546            int len = ancestorIds.Length;
 01547            if (len == 0)
 1548            {
 01549                return;
 1550            }
 1551
 01552            var parents = new BaseItem[len];
 01553            for (int i = 0; i < len; i++)
 1554            {
 01555                parents[i] = GetItemById(ancestorIds[i]) ?? throw new ArgumentException($"Failed to find parent with id:
 01556                if (parents[i] is not (ICollectionFolder or UserView))
 1557                {
 01558                    return;
 1559                }
 1560            }
 1561
 1562            // Optimize by querying against top level views
 01563            query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
 01564            query.AncestorIds = [];
 1565
 1566            // Prevent searching in all libraries due to empty filter
 01567            if (query.TopParentIds.Length == 0)
 1568            {
 01569                query.TopParentIds = [Guid.NewGuid()];
 1570            }
 01571        }
 1572
 1573        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
 1574        {
 01575            if (query.User is not null)
 1576            {
 01577                AddUserToQuery(query, query.User);
 1578            }
 1579
 01580            SetTopParentOrAncestorIds(query);
 01581            return _itemRepository.GetAlbumArtists(query);
 1582        }
 1583
 1584        public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query)
 1585        {
 111586            if (query.Recursive && !query.ParentId.IsEmpty())
 1587            {
 101588                var parent = GetItemById(query.ParentId);
 101589                if (parent is not null)
 1590                {
 101591                    SetTopParentIdsOrAncestors(query, [parent]);
 1592                }
 1593            }
 1594
 111595            if (query.User is not null)
 1596            {
 11597                AddUserToQuery(query, query.User);
 1598            }
 1599
 111600            if (query.EnableTotalRecordCount)
 1601            {
 11602                return _itemRepository.GetItems(query);
 1603            }
 1604
 101605            return new QueryResult<BaseItem>(
 101606                query.StartIndex,
 101607                null,
 101608                _itemRepository.GetItemList(query));
 1609        }
 1610
 1611        private void SetTopParentIdsOrAncestors(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents)
 1612        {
 501613            if (parents.All(i => i is ICollectionFolder || i is UserView))
 1614            {
 1615                // Optimize by querying against top level views
 101616                query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
 1617
 1618                // Prevent searching in all libraries due to empty filter
 101619                if (query.TopParentIds.Length == 0)
 1620                {
 101621                    query.TopParentIds = [Guid.NewGuid()];
 1622                }
 1623            }
 1624            else
 1625            {
 1626                // We need to be able to query from any arbitrary ancestor up the tree
 401627                query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).ToArray();
 1628
 1629                // Prevent searching in all libraries due to empty filter
 401630                if (query.AncestorIds.Length == 0)
 1631                {
 01632                    query.AncestorIds = [Guid.NewGuid()];
 1633                }
 1634            }
 1635
 501636            query.Parent = null;
 501637        }
 1638
 1639        private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
 1640        {
 21641            if (query.AncestorIds.Length == 0 &&
 21642                query.ParentId.IsEmpty() &&
 21643                query.ChannelIds.Count == 0 &&
 21644                query.TopParentIds.Length == 0 &&
 21645                string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) &&
 21646                string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) &&
 21647                query.ItemIds.Length == 0)
 1648            {
 11649                var userViews = UserViewManager.GetUserViews(new UserViewQuery
 11650                {
 11651                    User = user,
 11652                    IncludeHidden = true,
 11653                    IncludeExternalContent = allowExternalContent
 11654                });
 1655
 11656                query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).ToArray();
 1657
 1658                // Prevent searching in all libraries due to empty filter
 11659                if (query.TopParentIds.Length == 0)
 1660                {
 11661                    query.TopParentIds = [Guid.NewGuid()];
 1662                }
 1663            }
 21664        }
 1665
 1666        private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User? user)
 1667        {
 101668            if (item is UserView view)
 1669            {
 01670                if (view.ViewType == CollectionType.livetv)
 1671                {
 01672                    return [view.Id];
 1673                }
 1674
 1675                // Translate view into folders
 01676                if (!view.DisplayParentId.IsEmpty())
 1677                {
 01678                    var displayParent = GetItemById(view.DisplayParentId);
 01679                    if (displayParent is not null)
 1680                    {
 01681                        return GetTopParentIdsForQuery(displayParent, user);
 1682                    }
 1683
 01684                    return [];
 1685                }
 1686
 01687                if (!view.ParentId.IsEmpty())
 1688                {
 01689                    var displayParent = GetItemById(view.ParentId);
 01690                    if (displayParent is not null)
 1691                    {
 01692                        return GetTopParentIdsForQuery(displayParent, user);
 1693                    }
 1694
 01695                    return [];
 1696                }
 1697
 1698                // Handle grouping
 01699                if (user is not null && view.ViewType != CollectionType.unknown && UserView.IsEligibleForGrouping(view.V
 01700                    && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0)
 1701                {
 01702                    return GetUserRootFolder()
 01703                        .GetChildren(user, true)
 01704                        .OfType<CollectionFolder>()
 01705                        .Where(i => i.CollectionType is null || i.CollectionType == view.ViewType)
 01706                        .Where(i => user.IsFolderGrouped(i.Id))
 01707                        .SelectMany(i => GetTopParentIdsForQuery(i, user));
 1708                }
 1709
 01710                return [];
 1711            }
 1712
 101713            if (item is CollectionFolder collectionFolder)
 1714            {
 101715                return collectionFolder.PhysicalFolderIds;
 1716            }
 1717
 01718            var topParent = item.GetTopParent();
 01719            if (topParent is not null)
 1720            {
 01721                return [topParent.Id];
 1722            }
 1723
 01724            return [];
 1725        }
 1726
 1727        /// <summary>
 1728        /// Gets the intros.
 1729        /// </summary>
 1730        /// <param name="item">The item.</param>
 1731        /// <param name="user">The user.</param>
 1732        /// <returns>IEnumerable{System.String}.</returns>
 1733        public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
 1734        {
 1735            if (IntroProviders.Length == 0)
 1736            {
 1737                return [];
 1738            }
 1739
 1740            var tasks = IntroProviders
 1741                .Select(i => GetIntros(i, item, user));
 1742
 1743            var items = await Task.WhenAll(tasks).ConfigureAwait(false);
 1744
 1745            return items
 1746                .SelectMany(i => i)
 1747                .Select(ResolveIntro)
 1748                .Where(i => i is not null)!; // null values got filtered out
 1749        }
 1750
 1751        /// <summary>
 1752        /// Gets the intros.
 1753        /// </summary>
 1754        /// <param name="provider">The provider.</param>
 1755        /// <param name="item">The item.</param>
 1756        /// <param name="user">The user.</param>
 1757        /// <returns>Task&lt;IEnumerable&lt;IntroInfo&gt;&gt;.</returns>
 1758        private async Task<IEnumerable<IntroInfo>> GetIntros(IIntroProvider provider, BaseItem item, User user)
 1759        {
 1760            try
 1761            {
 1762                return await provider.GetIntros(item, user).ConfigureAwait(false);
 1763            }
 1764            catch (Exception ex)
 1765            {
 1766                _logger.LogError(ex, "Error getting intros");
 1767
 1768                return [];
 1769            }
 1770        }
 1771
 1772        /// <summary>
 1773        /// Resolves the intro.
 1774        /// </summary>
 1775        /// <param name="info">The info.</param>
 1776        /// <returns>Video.</returns>
 1777        private Video? ResolveIntro(IntroInfo info)
 1778        {
 01779            Video? video = null;
 1780
 01781            if (info.ItemId.HasValue)
 1782            {
 1783                // Get an existing item by Id
 01784                video = GetItemById(info.ItemId.Value) as Video;
 1785
 01786                if (video is null)
 1787                {
 01788                    _logger.LogError("Unable to locate item with Id {ID}.", info.ItemId.Value);
 1789                }
 1790            }
 01791            else if (!string.IsNullOrEmpty(info.Path))
 1792            {
 1793                try
 1794                {
 1795                    // Try to resolve the path into a video
 01796                    video = ResolvePath(_fileSystem.GetFileSystemInfo(info.Path)) as Video;
 1797
 01798                    if (video is null)
 1799                    {
 01800                        _logger.LogError("Intro resolver returned null for {Path}.", info.Path);
 1801                    }
 1802                    else
 1803                    {
 1804                        // Pull the saved db item that will include metadata
 01805                        var dbItem = GetItemById(video.Id) as Video;
 1806
 01807                        if (dbItem is not null)
 1808                        {
 01809                            video = dbItem;
 1810                        }
 1811                        else
 1812                        {
 01813                            return null;
 1814                        }
 1815                    }
 01816                }
 01817                catch (Exception ex)
 1818                {
 01819                    _logger.LogError(ex, "Error resolving path {Path}.", info.Path);
 01820                }
 1821            }
 1822            else
 1823            {
 01824                _logger.LogError("IntroProvider returned an IntroInfo with null Path and ItemId.");
 1825            }
 1826
 01827            return video;
 01828        }
 1829
 1830        /// <inheritdoc />
 1831        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortO
 1832        {
 11833            IOrderedEnumerable<BaseItem>? orderedItems = null;
 1834
 41835            foreach (var orderBy in sortBy.Select(o => GetComparer(o, user)).Where(c => c is not null))
 1836            {
 11837                if (orderBy is RandomComparer)
 1838                {
 01839                    var randomItems = items.ToArray();
 01840                    Random.Shared.Shuffle(randomItems);
 01841                    items = randomItems;
 1842                    // Items are no longer ordered at this point, so set orderedItems back to null
 01843                    orderedItems = null;
 1844                }
 11845                else if (orderedItems is null)
 1846                {
 11847                    orderedItems = sortOrder == SortOrder.Descending
 11848                        ? items.OrderByDescending(i => i, orderBy)
 11849                        : items.OrderBy(i => i, orderBy);
 1850                }
 1851                else
 1852                {
 01853                    orderedItems = sortOrder == SortOrder.Descending
 01854                        ? orderedItems!.ThenByDescending(i => i, orderBy)
 01855                        : orderedItems!.ThenBy(i => i, orderBy); // orderedItems is set during the first iteration
 1856                }
 1857            }
 1858
 11859            return orderedItems ?? items;
 1860        }
 1861
 1862        /// <inheritdoc />
 1863        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<(ItemSortBy OrderBy, Sort
 1864        {
 01865            IOrderedEnumerable<BaseItem>? orderedItems = null;
 1866
 01867            foreach (var (name, sortOrder) in orderBy)
 1868            {
 01869                var comparer = GetComparer(name, user);
 01870                if (comparer is null)
 1871                {
 1872                    continue;
 1873                }
 1874
 01875                if (comparer is RandomComparer)
 1876                {
 01877                    var randomItems = items.ToArray();
 01878                    Random.Shared.Shuffle(randomItems);
 01879                    items = randomItems;
 1880                    // Items are no longer ordered at this point, so set orderedItems back to null
 01881                    orderedItems = null;
 1882                }
 01883                else if (orderedItems is null)
 1884                {
 01885                    orderedItems = sortOrder == SortOrder.Descending
 01886                        ? items.OrderByDescending(i => i, comparer)
 01887                        : items.OrderBy(i => i, comparer);
 1888                }
 1889                else
 1890                {
 01891                    orderedItems = sortOrder == SortOrder.Descending
 01892                        ? orderedItems!.ThenByDescending(i => i, comparer)
 01893                        : orderedItems!.ThenBy(i => i, comparer); // orderedItems is set during the first iteration
 1894                }
 1895            }
 1896
 01897            return orderedItems ?? items;
 1898        }
 1899
 1900        /// <summary>
 1901        /// Gets the comparer.
 1902        /// </summary>
 1903        /// <param name="name">The name.</param>
 1904        /// <param name="user">The user.</param>
 1905        /// <returns>IBaseItemComparer.</returns>
 1906        private IBaseItemComparer? GetComparer(ItemSortBy name, User? user)
 1907        {
 11908            var comparer = Comparers.FirstOrDefault(c => name == c.Type);
 1909
 1910            // If it requires a user, create a new one, and assign the user
 11911            if (comparer is IUserBaseItemComparer)
 1912            {
 01913                var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType())!; // only null fo
 1914
 01915                userComparer.User = user;
 01916                userComparer.UserManager = _userManager;
 01917                userComparer.UserDataManager = _userDataManager;
 1918
 01919                return userComparer;
 1920            }
 1921
 11922            return comparer;
 1923        }
 1924
 1925        /// <inheritdoc />
 1926        public void CreateItem(BaseItem item, BaseItem? parent)
 1927        {
 01928            CreateItems([item], parent, CancellationToken.None);
 01929        }
 1930
 1931        /// <inheritdoc />
 1932        public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
 1933        {
 21934            _itemRepository.SaveItems(items, cancellationToken);
 1935
 81936            foreach (var item in items)
 1937            {
 21938                RegisterItem(item);
 1939            }
 1940
 21941            if (ItemAdded is not null)
 1942            {
 81943                foreach (var item in items)
 1944                {
 1945                    // With the live tv guide this just creates too much noise
 21946                    if (item.SourceType != SourceType.Library)
 1947                    {
 1948                        continue;
 1949                    }
 1950
 1951                    try
 1952                    {
 21953                        ItemAdded(
 21954                            this,
 21955                            new ItemChangeEventArgs
 21956                            {
 21957                                Item = item,
 21958                                Parent = parent ?? item.GetParent()
 21959                            });
 21960                    }
 01961                    catch (Exception ex)
 1962                    {
 01963                        _logger.LogError(ex, "Error in ItemAdded event handler");
 01964                    }
 1965                }
 1966            }
 21967        }
 1968
 1969        private bool ImageNeedsRefresh(ItemImageInfo image)
 1970        {
 01971            if (image.Path is not null && image.IsLocalFile)
 1972            {
 01973                if (image.Width == 0 || image.Height == 0 || string.IsNullOrEmpty(image.BlurHash))
 1974                {
 01975                    return true;
 1976                }
 1977
 1978                try
 1979                {
 01980                    return image.DateModified.Subtract(_fileSystem.GetLastWriteTimeUtc(image.Path)).Duration().TotalSeco
 1981                }
 01982                catch (Exception ex)
 1983                {
 01984                    _logger.LogError(ex, "Cannot get file info for {0}", image.Path);
 01985                    return false;
 1986                }
 1987            }
 1988
 01989            return image.Path is not null && !image.IsLocalFile;
 01990        }
 1991
 1992        /// <inheritdoc />
 1993        public async Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false)
 1994        {
 1995            ArgumentNullException.ThrowIfNull(item);
 1996
 1997            var outdated = forceUpdate
 1998                ? item.ImageInfos.Where(i => i.Path is not null).ToArray()
 1999                : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
 2000            // Skip image processing if current or live tv source
 2001            if (outdated.Length == 0 || item.SourceType != SourceType.Library)
 2002            {
 2003                RegisterItem(item);
 2004                return;
 2005            }
 2006
 2007            foreach (var img in outdated)
 2008            {
 2009                var image = img;
 2010                if (!img.IsLocalFile)
 2011                {
 2012                    try
 2013                    {
 2014                        var index = item.GetImageIndex(img);
 2015                        image = await ConvertImageToLocal(item, img, index, true).ConfigureAwait(false);
 2016                    }
 2017                    catch (ArgumentException)
 2018                    {
 2019                        _logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
 2020                        continue;
 2021                    }
 2022                    catch (Exception ex) when (ex is InvalidOperationException or IOException)
 2023                    {
 2024                        _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path);
 2025                        continue;
 2026                    }
 2027                    catch (HttpRequestException ex)
 2028                    {
 2029                        _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}. Http status code: {HttpStatus}", im
 2030                        continue;
 2031                    }
 2032                }
 2033
 2034                ImageDimensions size;
 2035                try
 2036                {
 2037                    size = _imageProcessor.GetImageDimensions(item, image);
 2038                    image.Width = size.Width;
 2039                    image.Height = size.Height;
 2040                }
 2041                catch (Exception ex)
 2042                {
 2043                    _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
 2044                    size = default;
 2045                    image.Width = 0;
 2046                    image.Height = 0;
 2047                }
 2048
 2049                try
 2050                {
 2051                    var blurhash = _imageProcessor.GetImageBlurHash(image.Path, size);
 2052                    image.BlurHash = blurhash;
 2053                }
 2054                catch (Exception ex)
 2055                {
 2056                    _logger.LogError(ex, "Cannot compute blurhash for {ImagePath}", image.Path);
 2057                    image.BlurHash = string.Empty;
 2058                }
 2059
 2060                try
 2061                {
 2062                    var modifiedDate = _fileSystem.GetLastWriteTimeUtc(image.Path);
 2063                    image.DateModified = modifiedDate;
 2064                }
 2065                catch (Exception ex)
 2066                {
 2067                    _logger.LogError(ex, "Cannot update DateModified for {ImagePath}", image.Path);
 2068                }
 2069            }
 2070
 2071            _itemRepository.SaveImages(item);
 2072
 2073            RegisterItem(item);
 2074        }
 2075
 2076        /// <inheritdoc />
 2077        public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, 
 2078        {
 2079            foreach (var item in items)
 2080            {
 2081                item.DateLastSaved = DateTime.UtcNow;
 2082                await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
 2083
 2084                // Modify again, so saved value is after write time of externally saved metadata
 2085                item.DateLastSaved = DateTime.UtcNow;
 2086            }
 2087
 2088            _itemRepository.SaveItems(items, cancellationToken);
 2089
 2090            if (ItemUpdated is not null)
 2091            {
 2092                foreach (var item in items)
 2093                {
 2094                    // With the live tv guide this just creates too much noise
 2095                    if (item.SourceType != SourceType.Library)
 2096                    {
 2097                        continue;
 2098                    }
 2099
 2100                    try
 2101                    {
 2102                        ItemUpdated(
 2103                            this,
 2104                            new ItemChangeEventArgs
 2105                            {
 2106                                Item = item,
 2107                                Parent = parent,
 2108                                UpdateReason = updateReason
 2109                            });
 2110                    }
 2111                    catch (Exception ex)
 2112                    {
 2113                        _logger.LogError(ex, "Error in ItemUpdated event handler");
 2114                    }
 2115                }
 2116            }
 2117        }
 2118
 2119        /// <inheritdoc />
 2120        public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cance
 852121            => UpdateItemsAsync([item], parent, updateReason, cancellationToken);
 2122
 2123        public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
 2124        {
 2125            if (item.IsFileProtocol)
 2126            {
 2127                await ProviderManager.SaveMetadataAsync(item, updateReason).ConfigureAwait(false);
 2128            }
 2129
 2130            await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
 2131        }
 2132
 2133        /// <summary>
 2134        /// Reports the item removed.
 2135        /// </summary>
 2136        /// <param name="item">The item.</param>
 2137        /// <param name="parent">The parent item.</param>
 2138        public void ReportItemRemoved(BaseItem item, BaseItem parent)
 2139        {
 12140            if (ItemRemoved is not null)
 2141            {
 2142                try
 2143                {
 12144                    ItemRemoved(
 12145                        this,
 12146                        new ItemChangeEventArgs
 12147                        {
 12148                            Item = item,
 12149                            Parent = parent
 12150                        });
 12151                }
 02152                catch (Exception ex)
 2153                {
 02154                    _logger.LogError(ex, "Error in ItemRemoved event handler");
 02155                }
 2156            }
 12157        }
 2158
 2159        /// <summary>
 2160        /// Retrieves the item.
 2161        /// </summary>
 2162        /// <param name="id">The id.</param>
 2163        /// <returns>BaseItem.</returns>
 2164        public BaseItem RetrieveItem(Guid id)
 2165        {
 2462166            return _itemRepository.RetrieveItem(id);
 2167        }
 2168
 2169        public List<Folder> GetCollectionFolders(BaseItem item)
 2170        {
 6382171            return GetCollectionFolders(item, GetUserRootFolder().Children.OfType<Folder>());
 2172        }
 2173
 2174        public List<Folder> GetCollectionFolders(BaseItem item, IEnumerable<Folder> allUserRootChildren)
 2175        {
 6702176            while (item is not null)
 2177            {
 6702178                var parent = item.GetParent();
 2179
 6702180                if (parent is AggregateFolder)
 2181                {
 2182                    break;
 2183                }
 2184
 6572185                if (parent is null)
 2186                {
 6252187                    var owner = item.GetOwner();
 2188
 6252189                    if (owner is null)
 2190                    {
 2191                        break;
 2192                    }
 2193
 02194                    item = owner;
 2195                }
 2196                else
 2197                {
 322198                    item = parent;
 2199                }
 2200            }
 2201
 6382202            if (item is null)
 2203            {
 02204                return new List<Folder>();
 2205            }
 2206
 6382207            return GetCollectionFoldersInternal(item, allUserRootChildren);
 2208        }
 2209
 2210        private static List<Folder> GetCollectionFoldersInternal(BaseItem item, IEnumerable<Folder> allUserRootChildren)
 2211        {
 6382212            return allUserRootChildren
 6382213                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.
 6382214                .ToList();
 2215        }
 2216
 2217        public LibraryOptions GetLibraryOptions(BaseItem item)
 2218        {
 4032219            if (item is CollectionFolder collectionFolder)
 2220            {
 462221                return collectionFolder.GetLibraryOptions();
 2222            }
 2223
 2224            // List.Find is more performant than FirstOrDefault due to enumerator allocation
 3572225            return GetCollectionFolders(item)
 3572226                .Find(folder => folder is CollectionFolder) is CollectionFolder collectionFolder2
 3572227                ? collectionFolder2.GetLibraryOptions()
 3572228                : new LibraryOptions();
 2229        }
 2230
 2231        public CollectionType? GetContentType(BaseItem item)
 2232        {
 422233            var configuredContentType = GetConfiguredContentType(item, false);
 422234            if (configuredContentType is not null)
 2235            {
 02236                return configuredContentType;
 2237            }
 2238
 422239            configuredContentType = GetConfiguredContentType(item, true);
 422240            if (configuredContentType is not null)
 2241            {
 02242                return configuredContentType;
 2243            }
 2244
 422245            return GetInheritedContentType(item);
 2246        }
 2247
 2248        public CollectionType? GetInheritedContentType(BaseItem item)
 2249        {
 422250            var type = GetTopFolderContentType(item);
 2251
 422252            if (type is not null)
 2253            {
 02254                return type;
 2255            }
 2256
 422257            return item.GetParents()
 422258                .Select(GetConfiguredContentType)
 422259                .LastOrDefault(i => i is not null);
 2260        }
 2261
 2262        public CollectionType? GetConfiguredContentType(BaseItem item)
 2263        {
 02264            return GetConfiguredContentType(item, false);
 2265        }
 2266
 2267        public CollectionType? GetConfiguredContentType(string path)
 2268        {
 02269            return GetContentTypeOverride(path, false);
 2270        }
 2271
 2272        public CollectionType? GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath)
 2273        {
 842274            if (item is ICollectionFolder collectionFolder)
 2275            {
 02276                return collectionFolder.CollectionType;
 2277            }
 2278
 842279            return GetContentTypeOverride(item.ContainingFolderPath, inheritConfiguredPath);
 2280        }
 2281
 2282        private CollectionType? GetContentTypeOverride(string path, bool inherit)
 2283        {
 1012284            var nameValuePair = _configurationManager.Configuration.ContentTypes
 1012285                                    .FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path)
 1012286                                                         || (inherit && !string.IsNullOrEmpty(i.Name)
 1012287                                                                     && _fileSystem.ContainsSubPath(i.Name, path)));
 1012288            if (Enum.TryParse<CollectionType>(nameValuePair?.Value, out var collectionType))
 2289            {
 02290                return collectionType;
 2291            }
 2292
 1012293            return null;
 2294        }
 2295
 2296        private CollectionType? GetTopFolderContentType(BaseItem item)
 2297        {
 422298            if (item is null)
 2299            {
 02300                return null;
 2301            }
 2302
 422303            while (!item.ParentId.IsEmpty())
 2304            {
 02305                var parent = item.GetParent();
 02306                if (parent is null || parent is AggregateFolder)
 2307                {
 2308                    break;
 2309                }
 2310
 02311                item = parent;
 2312            }
 2313
 422314            return GetUserRootFolder().Children
 422315                .OfType<ICollectionFolder>()
 422316                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.
 422317                .Select(i => i.CollectionType)
 422318                .FirstOrDefault(i => i is not null);
 2319        }
 2320
 2321        public UserView GetNamedView(
 2322            User user,
 2323            string name,
 2324            CollectionType? viewType,
 2325            string sortName)
 2326        {
 02327            return GetNamedView(user, name, Guid.Empty, viewType, sortName);
 2328        }
 2329
 2330        public UserView GetNamedView(
 2331            string name,
 2332            CollectionType viewType,
 2333            string sortName)
 2334        {
 02335            var path = Path.Combine(
 02336                _configurationManager.ApplicationPaths.InternalMetadataPath,
 02337                "views",
 02338                _fileSystem.GetValidFilename(viewType.ToString()));
 2339
 02340            var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView));
 2341
 02342            var item = GetItemById(id) as UserView;
 2343
 02344            var refresh = false;
 2345
 02346            if (item is null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase))
 2347            {
 02348                var info = Directory.CreateDirectory(path);
 02349                item = new UserView
 02350                {
 02351                    Path = path,
 02352                    Id = id,
 02353                    DateCreated = info.CreationTimeUtc,
 02354                    DateModified = info.LastWriteTimeUtc,
 02355                    Name = name,
 02356                    ViewType = viewType,
 02357                    ForcedSortName = sortName
 02358                };
 2359
 02360                CreateItem(item, null);
 2361
 02362                refresh = true;
 2363            }
 2364
 02365            if (refresh)
 2366            {
 02367                item.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResu
 02368                ProviderManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), Ref
 2369            }
 2370
 02371            return item;
 2372        }
 2373
 2374        public UserView GetNamedView(
 2375            User user,
 2376            string name,
 2377            Guid parentId,
 2378            CollectionType? viewType,
 2379            string sortName)
 2380        {
 02381            var parentIdString = parentId.IsEmpty()
 02382                ? null
 02383                : parentId.ToString("N", CultureInfo.InvariantCulture);
 02384            var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdStrin
 2385
 02386            var id = GetNewItemId(idValues, typeof(UserView));
 2387
 02388            var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N
 2389
 02390            var item = GetItemById(id) as UserView;
 2391
 02392            var isNew = false;
 2393
 02394            if (item is null)
 2395            {
 02396                var info = Directory.CreateDirectory(path);
 02397                item = new UserView
 02398                {
 02399                    Path = path,
 02400                    Id = id,
 02401                    DateCreated = info.CreationTimeUtc,
 02402                    DateModified = info.LastWriteTimeUtc,
 02403                    Name = name,
 02404                    ViewType = viewType,
 02405                    ForcedSortName = sortName,
 02406                    UserId = user.Id,
 02407                    DisplayParentId = parentId
 02408                };
 2409
 02410                CreateItem(item, null);
 2411
 02412                isNew = true;
 2413            }
 2414
 02415            var lastRefreshedUtc = item.DateLastRefreshed;
 02416            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 2417
 02418            if (!refresh && !item.DisplayParentId.IsEmpty())
 2419            {
 02420                var displayParent = GetItemById(item.DisplayParentId);
 02421                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 2422            }
 2423
 02424            if (refresh)
 2425            {
 02426                ProviderManager.QueueRefresh(
 02427                    item.Id,
 02428                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 02429                    {
 02430                        // Need to force save to increment DateLastSaved
 02431                        ForceSave = true
 02432                    },
 02433                    RefreshPriority.Normal);
 2434            }
 2435
 02436            return item;
 2437        }
 2438
 2439        public UserView GetShadowView(
 2440            BaseItem parent,
 2441            CollectionType? viewType,
 2442            string sortName)
 2443        {
 02444            ArgumentNullException.ThrowIfNull(parent);
 2445
 02446            var name = parent.Name;
 02447            var parentId = parent.Id;
 2448
 02449            var idValues = "38_namedview_" + name + parentId + (viewType?.ToString() ?? string.Empty);
 2450
 02451            var id = GetNewItemId(idValues, typeof(UserView));
 2452
 02453            var path = parent.Path;
 2454
 02455            var item = GetItemById(id) as UserView;
 2456
 02457            var isNew = false;
 2458
 02459            if (item is null)
 2460            {
 02461                var info = Directory.CreateDirectory(path);
 02462                item = new UserView
 02463                {
 02464                    Path = path,
 02465                    Id = id,
 02466                    DateCreated = info.CreationTimeUtc,
 02467                    DateModified = info.LastWriteTimeUtc,
 02468                    Name = name,
 02469                    ViewType = viewType,
 02470                    ForcedSortName = sortName,
 02471                    DisplayParentId = parentId
 02472                };
 2473
 02474                CreateItem(item, null);
 2475
 02476                isNew = true;
 2477            }
 2478
 02479            var lastRefreshedUtc = item.DateLastRefreshed;
 02480            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 2481
 02482            if (!refresh && !item.DisplayParentId.IsEmpty())
 2483            {
 02484                var displayParent = GetItemById(item.DisplayParentId);
 02485                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 2486            }
 2487
 02488            if (refresh)
 2489            {
 02490                ProviderManager.QueueRefresh(
 02491                    item.Id,
 02492                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 02493                    {
 02494                        // Need to force save to increment DateLastSaved
 02495                        ForceSave = true
 02496                    },
 02497                    RefreshPriority.Normal);
 2498            }
 2499
 02500            return item;
 2501        }
 2502
 2503        public UserView GetNamedView(
 2504            string name,
 2505            Guid parentId,
 2506            CollectionType? viewType,
 2507            string sortName,
 2508            string uniqueId)
 2509        {
 02510            ArgumentException.ThrowIfNullOrEmpty(name);
 2511
 02512            var parentIdString = parentId.IsEmpty()
 02513                ? null
 02514                : parentId.ToString("N", CultureInfo.InvariantCulture);
 02515            var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.E
 02516            if (!string.IsNullOrEmpty(uniqueId))
 2517            {
 02518                idValues += uniqueId;
 2519            }
 2520
 02521            var id = GetNewItemId(idValues, typeof(UserView));
 2522
 02523            var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N
 2524
 02525            var item = GetItemById(id) as UserView;
 2526
 02527            var isNew = false;
 2528
 02529            if (item is null)
 2530            {
 02531                var info = Directory.CreateDirectory(path);
 02532                item = new UserView
 02533                {
 02534                    Path = path,
 02535                    Id = id,
 02536                    DateCreated = info.CreationTimeUtc,
 02537                    DateModified = info.LastWriteTimeUtc,
 02538                    Name = name,
 02539                    ViewType = viewType,
 02540                    ForcedSortName = sortName,
 02541                    DisplayParentId = parentId
 02542                };
 2543
 02544                CreateItem(item, null);
 2545
 02546                isNew = true;
 2547            }
 2548
 02549            if (viewType != item.ViewType)
 2550            {
 02551                item.ViewType = viewType;
 02552                item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult
 2553            }
 2554
 02555            var lastRefreshedUtc = item.DateLastRefreshed;
 02556            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 2557
 02558            if (!refresh && !item.DisplayParentId.IsEmpty())
 2559            {
 02560                var displayParent = GetItemById(item.DisplayParentId);
 02561                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 2562            }
 2563
 02564            if (refresh)
 2565            {
 02566                ProviderManager.QueueRefresh(
 02567                    item.Id,
 02568                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 02569                    {
 02570                        // Need to force save to increment DateLastSaved
 02571                        ForceSave = true
 02572                    },
 02573                    RefreshPriority.Normal);
 2574            }
 2575
 02576            return item;
 2577        }
 2578
 2579        public BaseItem GetParentItem(Guid? parentId, Guid? userId)
 2580        {
 32581            if (parentId.HasValue)
 2582            {
 02583                return GetItemById(parentId.Value) ?? throw new ArgumentException($"Invalid parent id: {parentId.Value}"
 2584            }
 2585
 32586            if (!userId.IsNullOrEmpty())
 2587            {
 32588                return GetUserRootFolder();
 2589            }
 2590
 02591            return RootFolder;
 2592        }
 2593
 2594        /// <inheritdoc />
 2595        public void QueueLibraryScan()
 2596        {
 02597            _taskManager.QueueScheduledTask<RefreshMediaLibraryTask>();
 02598        }
 2599
 2600        /// <inheritdoc />
 2601        public int? GetSeasonNumberFromPath(string path, Guid? parentId)
 2602        {
 02603            var parentPath = parentId.HasValue ? GetItemById(parentId.Value)?.ContainingFolderPath : null;
 02604            return SeasonPathParser.Parse(path, parentPath, true, true).SeasonNumber;
 2605        }
 2606
 2607        /// <inheritdoc />
 2608        public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
 2609        {
 02610            var series = episode.Series;
 02611            bool? isAbsoluteNaming = series is not null && string.Equals(series.DisplayOrder, "absolute", StringComparis
 02612            if (!isAbsoluteNaming.Value)
 2613            {
 2614                // In other words, no filter applied
 02615                isAbsoluteNaming = null;
 2616            }
 2617
 02618            var resolver = new EpisodeResolver(_namingOptions);
 2619
 02620            var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
 2621
 02622            EpisodeInfo? episodeInfo = null;
 02623            if (episode.IsFileProtocol)
 2624            {
 02625                episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming);
 2626                // Resolve from parent folder if it's not the Season folder
 02627                var parent = episode.GetParent();
 02628                if (episodeInfo is null && parent.GetType() == typeof(Folder))
 2629                {
 02630                    episodeInfo = resolver.Resolve(parent.Path, true, null, null, isAbsoluteNaming);
 02631                    if (episodeInfo is not null)
 2632                    {
 2633                        // add the container
 02634                        episodeInfo.Container = Path.GetExtension(episode.Path)?.TrimStart('.');
 2635                    }
 2636                }
 2637            }
 2638
 02639            var changed = false;
 02640            if (episodeInfo is null)
 2641            {
 02642                return changed;
 2643            }
 2644
 02645            if (episodeInfo.IsByDate)
 2646            {
 02647                if (episode.IndexNumber.HasValue)
 2648                {
 02649                    episode.IndexNumber = null;
 02650                    changed = true;
 2651                }
 2652
 02653                if (episode.IndexNumberEnd.HasValue)
 2654                {
 02655                    episode.IndexNumberEnd = null;
 02656                    changed = true;
 2657                }
 2658
 02659                if (!episode.PremiereDate.HasValue)
 2660                {
 02661                    if (episodeInfo.Year.HasValue && episodeInfo.Month.HasValue && episodeInfo.Day.HasValue)
 2662                    {
 02663                        episode.PremiereDate = new DateTime(episodeInfo.Year.Value, episodeInfo.Month.Value, episodeInfo
 2664                    }
 2665
 02666                    if (episode.PremiereDate.HasValue)
 2667                    {
 02668                        changed = true;
 2669                    }
 2670                }
 2671
 02672                if (!episode.ProductionYear.HasValue)
 2673                {
 02674                    episode.ProductionYear = episodeInfo.Year;
 2675
 02676                    if (episode.ProductionYear.HasValue)
 2677                    {
 02678                        changed = true;
 2679                    }
 2680                }
 2681            }
 2682            else
 2683            {
 02684                if (!episode.IndexNumber.HasValue || forceRefresh)
 2685                {
 02686                    if (episode.IndexNumber != episodeInfo.EpisodeNumber)
 2687                    {
 02688                        changed = true;
 2689                    }
 2690
 02691                    episode.IndexNumber = episodeInfo.EpisodeNumber;
 2692                }
 2693
 02694                if (!episode.IndexNumberEnd.HasValue || forceRefresh)
 2695                {
 02696                    if (episode.IndexNumberEnd != episodeInfo.EndingEpisodeNumber)
 2697                    {
 02698                        changed = true;
 2699                    }
 2700
 02701                    episode.IndexNumberEnd = episodeInfo.EndingEpisodeNumber;
 2702                }
 2703
 02704                if (!episode.ParentIndexNumber.HasValue || forceRefresh)
 2705                {
 02706                    if (episode.ParentIndexNumber != episodeInfo.SeasonNumber)
 2707                    {
 02708                        changed = true;
 2709                    }
 2710
 02711                    episode.ParentIndexNumber = episodeInfo.SeasonNumber;
 2712                }
 2713            }
 2714
 02715            if (!episode.ParentIndexNumber.HasValue)
 2716            {
 02717                var season = episode.Season;
 2718
 02719                if (season is not null)
 2720                {
 02721                    episode.ParentIndexNumber = season.IndexNumber;
 2722                }
 2723
 02724                if (episode.ParentIndexNumber.HasValue)
 2725                {
 02726                    changed = true;
 2727                }
 2728            }
 2729
 02730            return changed;
 2731        }
 2732
 2733        public ItemLookupInfo ParseName(string name)
 2734        {
 02735            var namingOptions = _namingOptions;
 02736            var result = VideoResolver.CleanDateTime(name, namingOptions);
 2737
 02738            return new ItemLookupInfo
 02739            {
 02740                Name = VideoResolver.TryCleanString(result.Name, namingOptions, out var newName) ? newName : result.Name
 02741                Year = result.Year
 02742            };
 2743        }
 2744
 2745        public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, ID
 2746        {
 2747            // Apply .ignore rules
 2748            var filtered = fileSystemChildren.Where(c => !DotIgnoreIgnoreRule.IsIgnored(c, owner)).ToList();
 2749            var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions, libraryRoot: owner.Co
 2750            if (ownerVideoInfo is null)
 2751            {
 2752                yield break;
 2753            }
 2754
 2755            var count = filtered.Count;
 2756            for (var i = 0; i < count; i++)
 2757            {
 2758                var current = filtered[i];
 2759                if (current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name))
 2760                {
 2761                    var filesInSubFolder = _fileSystem.GetFiles(current.FullName, null, false, false);
 2762                    var filesInSubFolderList = filesInSubFolder.ToList();
 2763
 2764                    bool subFolderIsMixedFolder = filesInSubFolderList.Count > 1;
 2765
 2766                    foreach (var file in filesInSubFolderList)
 2767                    {
 2768                        if (!_extraResolver.TryGetExtraTypeForOwner(file.FullName, ownerVideoInfo, out var extraType))
 2769                        {
 2770                            continue;
 2771                        }
 2772
 2773                        var extra = GetExtra(file, extraType.Value, subFolderIsMixedFolder);
 2774                        if (extra is not null)
 2775                        {
 2776                            yield return extra;
 2777                        }
 2778                    }
 2779                }
 2780                else if (!current.IsDirectory && _extraResolver.TryGetExtraTypeForOwner(current.FullName, ownerVideoInfo
 2781                {
 2782                    var extra = GetExtra(current, extraType.Value, false);
 2783                    if (extra is not null)
 2784                    {
 2785                        yield return extra;
 2786                    }
 2787                }
 2788            }
 2789
 2790            BaseItem? GetExtra(FileSystemMetadata file, ExtraType extraType, bool isInMixedFolder)
 2791            {
 2792                var extra = ResolvePath(_fileSystem.GetFileInfo(file.FullName), directoryService, _extraResolver.GetReso
 2793                if (extra is not Video && extra is not Audio)
 2794                {
 2795                    return null;
 2796                }
 2797
 2798                // Try to retrieve it from the db. If we don't find it, use the resolved version
 2799                var itemById = GetItemById(extra.Id);
 2800                if (itemById is not null)
 2801                {
 2802                    extra = itemById;
 2803                }
 2804
 2805                // Only update extra type if it is more specific then the currently known extra type
 2806                if (extra.ExtraType is null or ExtraType.Unknown || extraType != ExtraType.Unknown)
 2807                {
 2808                    extra.ExtraType = extraType;
 2809                }
 2810
 2811                extra.ParentId = Guid.Empty;
 2812                extra.OwnerId = owner.Id;
 2813                extra.IsInMixedFolder = isInMixedFolder;
 2814                return extra;
 2815            }
 2816        }
 2817
 2818        public string GetPathAfterNetworkSubstitution(string path, BaseItem? ownerItem)
 2819        {
 122820            foreach (var map in _configurationManager.Configuration.PathSubstitutions)
 2821            {
 02822                if (path.TryReplaceSubPath(map.From, map.To, out var newPath))
 2823                {
 02824                    return newPath;
 2825                }
 2826            }
 2827
 62828            return path;
 2829        }
 2830
 2831        public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query)
 2832        {
 02833            return _peopleRepository.GetPeople(query);
 2834        }
 2835
 2836        public IReadOnlyList<PersonInfo> GetPeople(BaseItem item)
 2837        {
 62838            if (item.SupportsPeople)
 2839            {
 02840                var people = GetPeople(new InternalPeopleQuery
 02841                {
 02842                    ItemId = item.Id
 02843                });
 2844
 02845                if (people.Count > 0)
 2846                {
 02847                    return people;
 2848                }
 2849            }
 2850
 62851            return [];
 2852        }
 2853
 2854        public IReadOnlyList<Person> GetPeopleItems(InternalPeopleQuery query)
 2855        {
 02856            return _peopleRepository.GetPeopleNames(query)
 02857            .Select(i =>
 02858            {
 02859                try
 02860                {
 02861                    return GetPerson(i);
 02862                }
 02863                catch (Exception ex)
 02864                {
 02865                    _logger.LogError(ex, "Error getting person");
 02866                    return null;
 02867                }
 02868            })
 02869            .Where(i => i is not null)
 02870            .Where(i => query.User is null || i!.IsVisible(query.User))
 02871            .ToList()!; // null values are filtered out
 2872        }
 2873
 2874        public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query)
 2875        {
 02876            return _peopleRepository.GetPeopleNames(query);
 2877        }
 2878
 2879        public void UpdatePeople(BaseItem item, List<PersonInfo> people)
 2880        {
 02881            UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult();
 02882        }
 2883
 2884        /// <inheritdoc />
 2885        public async Task UpdatePeopleAsync(BaseItem item, IReadOnlyList<PersonInfo> people, CancellationToken cancellat
 2886        {
 2887            if (!item.SupportsPeople)
 2888            {
 2889                return;
 2890            }
 2891
 2892            if (people is not null)
 2893            {
 2894                people = people.Where(e => e is not null).ToArray();
 2895                _peopleRepository.UpdatePeople(item.Id, people);
 2896                await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
 2897            }
 2898        }
 2899
 2900        public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex, bool re
 2901        {
 2902            foreach (var url in image.Path.Split('|'))
 2903            {
 2904                try
 2905                {
 2906                    _logger.LogDebug("ConvertImageToLocal item {0} - image url: {1}", item.Id, url);
 2907
 2908                    await ProviderManager.SaveImage(item, url, image.Type, imageIndex, CancellationToken.None).Configure
 2909
 2910                    await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwai
 2911
 2912                    return item.GetImageInfo(image.Type, imageIndex);
 2913                }
 2914                catch (HttpRequestException ex)
 2915                {
 2916                    if (ex.StatusCode.HasValue
 2917                        && (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forb
 2918                    {
 2919                        _logger.LogDebug(ex, "Error downloading image {Url}", url);
 2920                        continue;
 2921                    }
 2922
 2923                    throw;
 2924                }
 2925            }
 2926
 2927            if (removeOnFailure)
 2928            {
 2929                // Remove this image to prevent it from retrying over and over
 2930                item.RemoveImage(image);
 2931                await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(fa
 2932            }
 2933
 2934            throw new InvalidOperationException("Unable to convert any images to local");
 2935        }
 2936
 2937        public async Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, b
 2938        {
 2939            if (string.IsNullOrWhiteSpace(name))
 2940            {
 2941                throw new ArgumentNullException(nameof(name));
 2942            }
 2943
 2944            name = _fileSystem.GetValidFilename(name.Trim());
 2945
 2946            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 2947
 2948            var existingNameCount = 1; // first numbered name will be 2
 2949            var virtualFolderPath = Path.Combine(rootFolderPath, name);
 2950            var originalName = name;
 2951            while (Directory.Exists(virtualFolderPath))
 2952            {
 2953                existingNameCount++;
 2954                name = originalName + existingNameCount;
 2955                virtualFolderPath = Path.Combine(rootFolderPath, name);
 2956            }
 2957
 2958            var mediaPathInfos = options.PathInfos;
 2959            if (mediaPathInfos is not null)
 2960            {
 2961                var invalidpath = mediaPathInfos.FirstOrDefault(i => !Directory.Exists(i.Path));
 2962                if (invalidpath is not null)
 2963                {
 2964                    throw new ArgumentException("The specified path does not exist: " + invalidpath.Path + ".");
 2965                }
 2966            }
 2967
 2968            LibraryMonitor.Stop();
 2969
 2970            try
 2971            {
 2972                Directory.CreateDirectory(virtualFolderPath);
 2973
 2974                if (collectionType is not null)
 2975                {
 2976                    var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collect
 2977
 2978                    FileHelper.CreateEmpty(path);
 2979                }
 2980
 2981                CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
 2982
 2983                if (mediaPathInfos is not null)
 2984                {
 2985                    foreach (var path in mediaPathInfos)
 2986                    {
 2987                        AddMediaPathInternal(name, path, false);
 2988                    }
 2989                }
 2990            }
 2991            finally
 2992            {
 2993                if (refreshLibrary)
 2994                {
 2995                    await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false);
 2996
 2997                    StartScanInBackground();
 2998                }
 2999                else
 3000                {
 3001                    // Need to add a delay here or directory watchers may still pick up the changes
 3002                    await Task.Delay(1000).ConfigureAwait(false);
 3003                    LibraryMonitor.Start();
 3004                }
 3005            }
 3006        }
 3007
 3008        private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
 3009        {
 3010            foreach (var person in people)
 3011            {
 3012                cancellationToken.ThrowIfCancellationRequested();
 3013
 3014                var itemUpdateType = ItemUpdateType.MetadataDownload;
 3015                var saveEntity = false;
 3016                var createEntity = false;
 3017                var personEntity = GetPerson(person.Name);
 3018
 3019                if (personEntity is null)
 3020                {
 3021                    try
 3022                    {
 3023                        var path = Person.GetPath(person.Name);
 3024                        var info = Directory.CreateDirectory(path);
 3025                        personEntity = new Person()
 3026                        {
 3027                            Name = person.Name,
 3028                            Id = GetItemByNameId<Person>(path),
 3029                            DateCreated = info.CreationTimeUtc,
 3030                            DateModified = info.LastWriteTimeUtc,
 3031                            Path = path
 3032                        };
 3033
 3034                        personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
 3035                        saveEntity = true;
 3036                        createEntity = true;
 3037                    }
 3038                    catch (Exception ex)
 3039                    {
 3040                        _logger.LogWarning(ex, "Failed to create person {Name}", person.Name);
 3041                        continue;
 3042                    }
 3043                }
 3044
 3045                foreach (var id in person.ProviderIds)
 3046                {
 3047                    if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase)
 3048                    {
 3049                        personEntity.SetProviderId(id.Key, id.Value);
 3050                        saveEntity = true;
 3051                    }
 3052                }
 3053
 3054                if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary))
 3055                {
 3056                    personEntity.SetImage(
 3057                        new ItemImageInfo
 3058                        {
 3059                            Path = person.ImageUrl,
 3060                            Type = ImageType.Primary
 3061                        },
 3062                        0);
 3063
 3064                    saveEntity = true;
 3065                    itemUpdateType = ItemUpdateType.ImageUpdate;
 3066                }
 3067
 3068                if (saveEntity)
 3069                {
 3070                    if (createEntity)
 3071                    {
 3072                        CreateItems([personEntity], null, CancellationToken.None);
 3073                    }
 3074
 3075                    await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
 3076                    personEntity.DateLastSaved = DateTime.UtcNow;
 3077
 3078                    CreateItems([personEntity], null, CancellationToken.None);
 3079                }
 3080            }
 3081        }
 3082
 3083        private void StartScanInBackground()
 3084        {
 33085            Task.Run(() =>
 33086            {
 33087                // No need to start if scanning the library because it will handle it
 33088                ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
 33089            });
 33090        }
 3091
 3092        public void AddMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
 3093        {
 13094            AddMediaPathInternal(virtualFolderName, mediaPath, true);
 03095        }
 3096
 3097        private void AddMediaPathInternal(string virtualFolderName, MediaPathInfo pathInfo, bool saveLibraryOptions)
 3098        {
 13099            ArgumentNullException.ThrowIfNull(pathInfo);
 3100
 13101            var path = pathInfo.Path;
 3102
 13103            if (string.IsNullOrWhiteSpace(path))
 3104            {
 03105                throw new ArgumentException(nameof(path));
 3106            }
 3107
 13108            if (!Directory.Exists(path))
 3109            {
 13110                throw new FileNotFoundException("The path does not exist.");
 3111            }
 3112
 03113            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 03114            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3115
 03116            var shortcutFilename = Path.GetFileNameWithoutExtension(path);
 3117
 03118            var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
 3119
 03120            while (File.Exists(lnk))
 3121            {
 03122                shortcutFilename += "1";
 03123                lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
 3124            }
 3125
 03126            _fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
 3127
 03128            RemoveContentTypeOverrides(path);
 3129
 03130            if (saveLibraryOptions)
 3131            {
 03132                var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3133
 03134                libraryOptions.PathInfos = [.. libraryOptions.PathInfos, pathInfo];
 3135
 03136                SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
 3137
 03138                CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 3139            }
 03140        }
 3141
 3142        public void UpdateMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
 3143        {
 03144            ArgumentNullException.ThrowIfNull(mediaPath);
 3145
 03146            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 03147            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3148
 03149            var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3150
 03151            SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
 3152
 03153            CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 03154        }
 3155
 3156        private void SyncLibraryOptionsToLocations(string virtualFolderPath, LibraryOptions options)
 3157        {
 03158            var topLibraryFolders = GetUserRootFolder().Children.ToList();
 03159            var info = GetVirtualFolderInfo(virtualFolderPath, topLibraryFolders, null);
 3160
 03161            if (info.Locations.Length > 0 && info.Locations.Length != options.PathInfos.Length)
 3162            {
 03163                var list = options.PathInfos.ToList();
 3164
 03165                foreach (var location in info.Locations)
 3166                {
 03167                    if (!list.Any(i => string.Equals(i.Path, location, StringComparison.Ordinal)))
 3168                    {
 03169                        list.Add(new MediaPathInfo(location));
 3170                    }
 3171                }
 3172
 03173                options.PathInfos = list.ToArray();
 3174            }
 03175        }
 3176
 3177        public async Task RemoveVirtualFolder(string name, bool refreshLibrary)
 3178        {
 3179            if (string.IsNullOrWhiteSpace(name))
 3180            {
 3181                throw new ArgumentNullException(nameof(name));
 3182            }
 3183
 3184            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 3185
 3186            var path = Path.Combine(rootFolderPath, name);
 3187
 3188            if (!Directory.Exists(path))
 3189            {
 3190                throw new FileNotFoundException("The media folder does not exist");
 3191            }
 3192
 3193            LibraryMonitor.Stop();
 3194
 3195            try
 3196            {
 3197                Directory.Delete(path, true);
 3198            }
 3199            finally
 3200            {
 3201                CollectionFolder.OnCollectionFolderChange();
 3202
 3203                if (refreshLibrary)
 3204                {
 3205                    await ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
 3206
 3207                    StartScanInBackground();
 3208                }
 3209                else
 3210                {
 3211                    // Need to add a delay here or directory watchers may still pick up the changes
 3212                    await Task.Delay(1000).ConfigureAwait(false);
 3213                    LibraryMonitor.Start();
 3214                }
 3215            }
 3216        }
 3217
 3218        private void RemoveContentTypeOverrides(string path)
 3219        {
 03220            if (string.IsNullOrWhiteSpace(path))
 3221            {
 03222                throw new ArgumentNullException(nameof(path));
 3223            }
 3224
 03225            List<NameValuePair>? removeList = null;
 3226
 03227            foreach (var contentType in _configurationManager.Configuration.ContentTypes)
 3228            {
 03229                if (string.IsNullOrWhiteSpace(contentType.Name)
 03230                    || _fileSystem.AreEqual(path, contentType.Name)
 03231                    || _fileSystem.ContainsSubPath(path, contentType.Name))
 3232                {
 03233                    (removeList ??= new()).Add(contentType);
 3234                }
 3235            }
 3236
 03237            if (removeList is not null)
 3238            {
 03239                _configurationManager.Configuration.ContentTypes = _configurationManager.Configuration.ContentTypes
 03240                    .Except(removeList)
 03241                    .ToArray();
 3242
 03243                _configurationManager.SaveConfiguration();
 3244            }
 03245        }
 3246
 3247        public void RemoveMediaPath(string virtualFolderName, string mediaPath)
 3248        {
 13249            ArgumentException.ThrowIfNullOrEmpty(mediaPath);
 3250
 13251            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 13252            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3253
 13254            if (!Directory.Exists(virtualFolderPath))
 3255            {
 13256                throw new FileNotFoundException(
 13257                    string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolder
 3258            }
 3259
 03260            var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
 03261                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCa
 03262                .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, String
 3263
 03264            if (!string.IsNullOrEmpty(shortcut))
 3265            {
 03266                _fileSystem.DeleteFile(shortcut);
 3267            }
 3268
 03269            var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3270
 03271            libraryOptions.PathInfos = libraryOptions
 03272                .PathInfos
 03273                .Where(i => !string.Equals(i.Path, mediaPath, StringComparison.Ordinal))
 03274                .ToArray();
 3275
 03276            CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 03277        }
 3278
 3279        private static bool ItemIsVisible(BaseItem? item, User? user)
 3280        {
 213281            if (item is null)
 3282            {
 213283                return false;
 3284            }
 3285
 03286            if (user is null)
 3287            {
 03288                return true;
 3289            }
 3290
 03291            return item is UserRootFolder || item.IsVisibleStandalone(user);
 3292        }
 3293    }
 3294}

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)
IsInternalItem(MediaBrowser.Controller.Entities.BaseItem)
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>)
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)
GetItemCounts(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)